Skip to content

Commit 39cf398

Browse files
committed
Improve TLS failure detection and fix version to 0.1.9
1 parent 5b95900 commit 39cf398

File tree

2 files changed

+60
-50
lines changed

2 files changed

+60
-50
lines changed

chatifier/utils.py

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
logger = 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} failed: {e}")
6373
return False, None, is_tls_error
6474

6575

6676
def 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

7585
def 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

95105
def 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

120130
def 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

140150
def 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

165175
def 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

199209
def 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

216226
def 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 []

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "llm-chatifier"
7-
version = "0.1.8"
7+
version = "0.1.9"
88
description = "A universal terminal chat client that auto-detects and connects to any chat API endpoint"
99
readme = "README.md"
1010
requires-python = ">=3.8"

0 commit comments

Comments
 (0)