1919logger  =  logging .getLogger (__name__ )
2020
2121
22- def  try_connection (url : str , headers : Optional [Dict [str , str ]] =  None , timeout : float  =  5.0 ) ->  Tuple [bool , Optional ['httpx.Response' ], bool ]:
22+ def  try_connection (url : str , headers : Optional [Dict [str , str ]] =  None ,
23+                    timeout : float  =  5.0 ) ->  Tuple [bool , Optional ['httpx.Response' ], bool ]:
2324    """Try to connect to a URL and return success status. 
24-      
25+ 
2526    Args: 
2627        url: URL to test 
2728        headers: Optional headers to send 
2829        timeout: Request timeout in seconds 
29-      
30+ 
3031    Returns: 
3132        Tuple of (success, response, is_tls_failure) where: 
3233        - success is bool 
@@ -35,7 +36,7 @@ def try_connection(url: str, headers: Optional[Dict[str, str]] = None, timeout:
3536    """ 
3637    if  httpx  is  None :
3738        raise  ImportError ("httpx is required for try_connection" )
38-      
39+ 
3940    try :
4041        with  httpx .Client (timeout = timeout , verify = False , follow_redirects = True ) as  client :
4142            # Try HEAD first (lighter), but immediately fall back to GET if 405 (Method Not Allowed) 
@@ -49,23 +50,32 @@ def try_connection(url: str, headers: Optional[Dict[str, str]] = None, timeout:
4950                            continue   # Try GET instead 
5051                        logger .debug (f"{ method } { url } { response .status_code }  )
5152                        return  True , response , False 
52-                 except  httpx .RequestError :
53+                 except  httpx .RequestError  as  req_err :
54+                     # Check if this is a TLS-related error 
55+                     error_str  =  str (req_err ).lower ()
56+                     is_tls_error  =  (any (tls_term  in  error_str  for  tls_term  in  [
57+                         'ssl' , 'tls' , 'handshake' , 'certificate' , 'cert' , 'timeout' 
58+                     ]) and  'https'  in  url .lower ())
59+ 
60+                     if  is_tls_error :
61+                         logger .debug (f"TLS error on { method } { url } { req_err }  )
62+                         return  False , None , True 
5363                    continue 
54-          
64+ 
5565        return  False , None , False 
56-      
66+ 
5767    except  Exception  as  e :
5868        error_str  =  str (e ).lower ()
5969        is_tls_error  =  any (tls_term  in  error_str  for  tls_term  in  [
60-             'ssl' , 'tls' , 'handshake' , 'certificate' , 'cert' 
61-         ])
70+             'ssl' , 'tls' , 'handshake' , 'certificate' , 'cert' ,  'timeout' 
71+         ])  and   'https'   in   url . lower () 
6272        logger .debug (f"Connection to { url } { e }  )
6373        return  False , None , is_tls_error 
6474
6575
6676def  prompt_for_token () ->  str :
6777    """Securely prompt user for API token. 
68-      
78+ 
6979    Returns: 
7080        API token string 
7181    """ 
@@ -74,17 +84,17 @@ def prompt_for_token() -> str:
7484
7585def  build_base_url (host : str , port : int , use_https : bool  =  True ) ->  str :
7686    """Build a proper base URL from components. 
77-      
87+ 
7888    Args: 
7989        host: Hostname or IP 
8090        port: Port number 
8191        use_https: Whether to use HTTPS 
82-      
92+ 
8393    Returns: 
8494        Formatted base URL 
8595    """ 
8696    scheme  =  "https"  if  use_https  else  "http" 
87-      
97+ 
8898    # Don't add port for standard ports 
8999    if  (use_https  and  port  ==  443 ) or  (not  use_https  and  port  ==  80 ):
90100        return  f"{ scheme } { host }  
@@ -94,10 +104,10 @@ def build_base_url(host: str, port: int, use_https: bool = True) -> str:
94104
95105def  extract_error_message (response : 'httpx.Response' ) ->  str :
96106    """Extract a meaningful error message from an HTTP response. 
97-      
107+ 
98108    Args: 
99109        response: HTTP response object 
100-      
110+ 
101111    Returns: 
102112        Error message string 
103113    """ 
@@ -119,16 +129,16 @@ def extract_error_message(response: 'httpx.Response') -> str:
119129
120130def  is_auth_error (response : 'httpx.Response' ) ->  bool :
121131    """Check if response indicates an authentication error. 
122-      
132+ 
123133    Args: 
124134        response: HTTP response object 
125-      
135+ 
126136    Returns: 
127137        True if auth error, False otherwise 
128138    """ 
129139    if  response .status_code  in  [401 , 403 ]:
130140        return  True 
131-      
141+ 
132142    try :
133143        text  =  response .text .lower ()
134144        auth_keywords  =  ['unauthorized' , 'forbidden' , 'authentication' , 'token' , 'api key' ]
@@ -139,35 +149,35 @@ def is_auth_error(response: 'httpx.Response') -> bool:
139149
140150def  format_model_name (model : str ) ->  str :
141151    """Format model name for display. 
142-      
152+ 
143153    Args: 
144154        model: Raw model name 
145-      
155+ 
146156    Returns: 
147157        Formatted model name 
148158    """ 
149159    # Remove common prefixes/suffixes for cleaner display 
150160    prefixes_to_remove  =  ['text-' , 'chat-' , 'gpt-' ]
151161    suffixes_to_remove  =  ['-latest' , '-preview' ]
152-      
162+ 
153163    formatted  =  model 
154164    for  prefix  in  prefixes_to_remove :
155165        if  formatted .startswith (prefix ):
156166            formatted  =  formatted [len (prefix ):]
157-      
167+ 
158168    for  suffix  in  suffixes_to_remove :
159169        if  formatted .endswith (suffix ):
160170            formatted  =  formatted [:- len (suffix )]
161-      
171+ 
162172    return  formatted 
163173
164174
165175def  parse_host_input (host_input : str ) ->  Tuple [str , Optional [int ], bool ]:
166176    """Parse user input that could be IP, hostname, or full URL. 
167-      
177+ 
168178    Args: 
169179        host_input: User input (IP, hostname, or URL) 
170-      
180+ 
171181    Returns: 
172182        Tuple of (hostname, port, use_https) 
173183    """ 
@@ -178,7 +188,7 @@ def parse_host_input(host_input: str) -> Tuple[str, Optional[int], bool]:
178188        port  =  parsed .port 
179189        use_https  =  parsed .scheme  ==  'https' 
180190        return  hostname , port , use_https 
181-      
191+ 
182192    # If it contains a port, split it 
183193    if  ':'  in  host_input  and  not  host_input .count (':' ) >  1 :  # Not IPv6 
184194        try :
@@ -187,7 +197,7 @@ def parse_host_input(host_input: str) -> Tuple[str, Optional[int], bool]:
187197            return  hostname , port , False   # Default to HTTP for IP:port format 
188198        except  ValueError :
189199            pass 
190-      
200+ 
191201    # Otherwise, treat as hostname/IP with no port specified 
192202    # For domain names like "api.anthropic.com", default to HTTPS 
193203    if  '.'  in  host_input  and  not  host_input .replace ('.' , '' ).replace ('-' , '' ).isdigit ():
@@ -198,14 +208,14 @@ def parse_host_input(host_input: str) -> Tuple[str, Optional[int], bool]:
198208
199209def  find_api_key_in_env (api_type : str , base_url : str  =  "" ) ->  Optional [str ]:
200210    """Smart environment variable detection for API keys. 
201-      
211+ 
202212    Searches environment variables for API keys that match the detected API type. 
203213    Works across Linux, Windows, and macOS. 
204-      
214+ 
205215    Args: 
206216        api_type: Detected API type (openai, anthropic, etc.) 
207217        base_url: Base URL to help with matching (optional) 
208-      
218+ 
209219    Returns: 
210220        Best matching API key or None if not found 
211221    """ 
@@ -215,44 +225,44 @@ def find_api_key_in_env(api_type: str, base_url: str = "") -> Optional[str]:
215225
216226def  find_all_api_keys_in_env (api_type : str , base_url : str  =  "" ) ->  List [str ]:
217227    """Smart environment variable detection for API keys - returns all matches in priority order. 
218-      
228+ 
219229    Searches environment variables for API keys that match the detected API type. 
220230    Works across Linux, Windows, and macOS. 
221-      
231+ 
222232    Args: 
223233        api_type: Detected API type (openai, anthropic, etc.) 
224234        base_url: Base URL to help with matching (optional) 
225-      
235+ 
226236    Returns: 
227237        List of API keys in priority order (best match first) 
228238    """ 
229239    # Get all environment variables 
230240    env_vars  =  dict (os .environ )
231-      
241+ 
232242    # Filter for variables that look like API keys 
233243    api_key_vars  =  {}
234244    for  name , value  in  env_vars .items ():
235245        name_upper  =  name .upper ()
236-         if  ('API_KEY'  in  name_upper  or   
237-             'APIKEY'  in  name_upper  or   
246+         if  ('API_KEY'  in  name_upper  or 
247+             'APIKEY'  in  name_upper  or 
238248            'API_TOKEN'  in  name_upper  or 
239249            ('TOKEN'  in  name_upper  and  len (value ) >  10 )):  # Basic sanity check 
240250            api_key_vars [name ] =  value 
241-      
251+ 
242252    if  not  api_key_vars :
243253        return  []
244-      
254+ 
245255    # Score each API key variable based on how well it matches 
246256    scored_vars  =  []
247-      
257+ 
248258    for  var_name , var_value  in  api_key_vars .items ():
249259        score  =  0 
250260        var_name_lower  =  var_name .lower ()
251-          
261+ 
252262        # Primary matching: API type name in variable name 
253263        if  api_type .lower () in  var_name_lower :
254264            score  +=  100 
255-          
265+ 
256266        # Secondary matching: related terms 
257267        api_terms  =  {
258268            'openai' : ['openai' , 'gpt' ],
@@ -262,12 +272,12 @@ def find_all_api_keys_in_env(api_type: str, base_url: str = "") -> List[str]:
262272            'cohere' : ['cohere' , 'co' ],
263273            'ollama' : ['ollama' ]
264274        }
265-          
275+ 
266276        if  api_type  in  api_terms :
267277            for  term  in  api_terms [api_type ]:
268278                if  term  in  var_name_lower :
269279                    score  +=  50 
270-          
280+ 
271281        # Tertiary matching: URL-based hints 
272282        if  base_url :
273283            url_lower  =  base_url .lower ()
@@ -281,25 +291,25 @@ def find_all_api_keys_in_env(api_type: str, base_url: str = "") -> List[str]:
281291                score  +=  30 
282292            elif  'cohere'  in  url_lower  and  'cohere'  in  var_name_lower :
283293                score  +=  30 
284-          
294+ 
285295        # Bonus for common naming patterns 
286296        if  re .match (r'.*API_?KEY$' , var_name .upper ()):
287297            score  +=  10 
288-          
298+ 
289299        # Penalty for very generic names 
290300        generic_names  =  ['API_KEY' , 'APIKEY' , 'TOKEN' , 'API_TOKEN' ]
291301        if  var_name .upper () in  generic_names :
292302            score  -=  20 
293-          
303+ 
294304        scored_vars .append ((score , var_name , var_value ))
295-      
305+ 
296306    # Sort by score (highest first) and return all matches 
297307    scored_vars .sort (reverse = True , key = lambda  x : x [0 ])
298-      
308+ 
299309    # Return all keys that scored above 0, or fallback to all found keys 
300310    good_matches  =  [var_value  for  score , var_name , var_value  in  scored_vars  if  score  >  0 ]
301311    if  good_matches :
302312        return  good_matches 
303-      
313+ 
304314    # If no good matches, return all API keys found as fallback 
305315    return  list (api_key_vars .values ()) if  api_key_vars  else  []
0 commit comments