@@ -151,6 +151,11 @@ def get_scheme_for_host(host: str, port: int) -> str:
151151 return "http"
152152
153153
154+ def is_full_url (host : str ) -> bool :
155+ """Check if host is already a full URL (starts with http:// or https://)."""
156+ return host .startswith ("http://" ) or host .startswith ("https://" )
157+
158+
154159def format_cooldown (seconds : int ) -> str :
155160 """Format cooldown seconds as human-readable string."""
156161 if seconds < 60 :
@@ -210,10 +215,57 @@ def _get_base_url(self) -> str:
210215 if not self .current_remote :
211216 return "http://127.0.0.1:8000"
212217 host = self .current_remote .get ("host" , "127.0.0.1" )
218+
219+ # If host is a full URL, use it directly (strip trailing slash)
220+ if is_full_url (host ):
221+ return host .rstrip ("/" )
222+
223+ # Otherwise construct from host:port
213224 port = self .current_remote .get ("port" , 8000 )
214225 scheme = get_scheme_for_host (host , port )
215226 return f"{ scheme } ://{ host } :{ port } "
216227
228+ def _build_endpoint_url (self , endpoint : str ) -> str :
229+ """
230+ Build a full endpoint URL with smart path handling.
231+
232+ Handles cases where base URL already contains a path (e.g., /v1):
233+ - Base: "https://api.example.com/v1", Endpoint: "/v1/quota-stats"
234+ -> "https://api.example.com/v1/quota-stats" (no duplication)
235+ - Base: "http://localhost:8000", Endpoint: "/v1/quota-stats"
236+ -> "http://localhost:8000/v1/quota-stats"
237+
238+ Args:
239+ endpoint: The endpoint path (e.g., "/v1/quota-stats")
240+
241+ Returns:
242+ Full URL string
243+ """
244+ base_url = self ._get_base_url ()
245+ endpoint = endpoint .lstrip ("/" )
246+
247+ # Check if base URL already ends with a path segment that matches
248+ # the start of the endpoint (e.g., base ends with /v1, endpoint starts with v1/)
249+ from urllib .parse import urlparse
250+
251+ parsed = urlparse (base_url )
252+ base_path = parsed .path .rstrip ("/" )
253+
254+ # If base has a path and endpoint starts with the same segment, avoid duplication
255+ if base_path :
256+ # e.g., base_path = "/v1", endpoint = "v1/quota-stats"
257+ # We want to produce "/v1/quota-stats", not "/v1/v1/quota-stats"
258+ base_segments = base_path .split ("/" )
259+ endpoint_segments = endpoint .split ("/" )
260+
261+ # Check if first endpoint segment matches last base segment
262+ if base_segments and endpoint_segments :
263+ if base_segments [- 1 ] == endpoint_segments [0 ]:
264+ # Skip the duplicated segment in endpoint
265+ endpoint = "/" .join (endpoint_segments [1 :])
266+
267+ return f"{ base_url } /{ endpoint } "
268+
217269 def check_connection (
218270 self , remote : Dict [str , Any ], timeout : float = 3.0
219271 ) -> Tuple [bool , str ]:
@@ -228,9 +280,18 @@ def check_connection(
228280 Tuple of (is_online, status_message)
229281 """
230282 host = remote .get ("host" , "127.0.0.1" )
231- port = remote .get ("port" , 8000 )
232- scheme = get_scheme_for_host (host , port )
233- url = f"{ scheme } ://{ host } :{ port } /"
283+
284+ # If host is a full URL, extract scheme and netloc to hit root
285+ if is_full_url (host ):
286+ from urllib .parse import urlparse
287+
288+ parsed = urlparse (host )
289+ # Hit the root domain, not the path (e.g., /v1 would 404)
290+ url = f"{ parsed .scheme } ://{ parsed .netloc } /"
291+ else :
292+ port = remote .get ("port" , 8000 )
293+ scheme = get_scheme_for_host (host , port )
294+ url = f"{ scheme } ://{ host } :{ port } /"
234295
235296 headers = {}
236297 if remote .get ("api_key" ):
@@ -262,7 +323,7 @@ def fetch_stats(self, provider: Optional[str] = None) -> Optional[Dict[str, Any]
262323 Returns:
263324 Stats dict or None on failure
264325 """
265- url = f" { self ._get_base_url () } /v1/quota-stats"
326+ url = self ._build_endpoint_url ( " /v1/quota-stats")
266327 if provider :
267328 url += f"?provider={ provider } "
268329
@@ -437,7 +498,7 @@ def post_action(
437498 Returns:
438499 Response dict or None on failure
439500 """
440- url = f" { self ._get_base_url () } /v1/quota-stats"
501+ url = self ._build_endpoint_url ( " /v1/quota-stats")
441502 payload = {
442503 "action" : action ,
443504 "scope" : scope ,
@@ -517,6 +578,14 @@ def show_summary_screen(self):
517578 remote_host = self .current_remote .get ("host" , "" ) if self .current_remote else ""
518579 remote_port = self .current_remote .get ("port" , "" ) if self .current_remote else ""
519580
581+ # Format connection display - handle full URLs
582+ if is_full_url (remote_host ):
583+ connection_display = remote_host
584+ elif remote_port :
585+ connection_display = f"{ remote_host } :{ remote_port } "
586+ else :
587+ connection_display = remote_host
588+
520589 # Calculate data age
521590 data_age = ""
522591 if self .cached_stats and self .cached_stats .get ("timestamp" ):
@@ -535,7 +604,7 @@ def show_summary_screen(self):
535604 )
536605 self .console .print ("━" * 78 )
537606 self .console .print (
538- f"Connected to: [bold]{ remote_name } [/bold] ({ remote_host } : { remote_port } ) "
607+ f"Connected to: [bold]{ remote_name } [/bold] ({ connection_display } ) "
539608 f"[green]✅[/green] | { data_age } "
540609 )
541610 self .console .print ()
@@ -1118,7 +1187,15 @@ def show_switch_remote_screen(self):
11181187 for idx , (remote , is_online , status_msg ) in enumerate (remote_status , 1 ):
11191188 name = remote .get ("name" , "Unknown" )
11201189 host = remote .get ("host" , "" )
1121- port = remote .get ("port" , 8000 )
1190+ port = remote .get ("port" , "" )
1191+
1192+ # Format connection display - handle full URLs
1193+ if is_full_url (host ):
1194+ connection_display = host
1195+ elif port :
1196+ connection_display = f"{ host } :{ port } "
1197+ else :
1198+ connection_display = host
11221199
11231200 is_current = name == current_name
11241201 current_marker = " (current)" if is_current else ""
@@ -1129,7 +1206,7 @@ def show_switch_remote_screen(self):
11291206 status_icon = f"[red]⚠️ { status_msg } [/red]"
11301207
11311208 self .console .print (
1132- f" { idx } . { name :<20} { host } : { port :<6 } { status_icon } { current_marker } "
1209+ f" { idx } . { name :<20} { connection_display :<30 } { status_icon } { current_marker } "
11331210 )
11341211
11351212 self .console .print ()
@@ -1253,23 +1330,37 @@ def _add_remote_dialog(self):
12531330 """Dialog to add a new remote."""
12541331 self .console .print ()
12551332 self .console .print ("[bold]Add New Remote[/bold]" )
1333+ self .console .print (
1334+ "[dim]For full URLs (e.g., https://api.example.com/v1), leave port empty[/dim]"
1335+ )
12561336 self .console .print ()
12571337
12581338 name = Prompt .ask ("Name" , default = "" ).strip ()
12591339 if not name :
12601340 self .console .print ("[dim]Cancelled.[/dim]" )
12611341 return
12621342
1263- host = Prompt .ask ("Host" , default = "" ).strip ()
1343+ host = Prompt .ask ("Host (or full URL) " , default = "" ).strip ()
12641344 if not host :
12651345 self .console .print ("[dim]Cancelled.[/dim]" )
12661346 return
12671347
1268- port_str = Prompt .ask ("Port" , default = "8000" ).strip ()
1269- try :
1270- port = int (port_str )
1271- except ValueError :
1272- port = 8000
1348+ # For full URLs, default to empty port
1349+ if is_full_url (host ):
1350+ port_default = ""
1351+ else :
1352+ port_default = "8000"
1353+
1354+ port_str = Prompt .ask (
1355+ "Port (empty for full URLs)" , default = port_default
1356+ ).strip ()
1357+ if port_str == "" :
1358+ port = ""
1359+ else :
1360+ try :
1361+ port = int (port_str )
1362+ except ValueError :
1363+ port = 8000
12731364
12741365 api_key = Prompt .ask ("API Key (optional)" , default = "" ).strip () or None
12751366
@@ -1284,16 +1375,30 @@ def _edit_remote_dialog(self, remote: Dict[str, Any]):
12841375 """Dialog to edit an existing remote."""
12851376 self .console .print ()
12861377 self .console .print (f"[bold]Edit Remote: { remote ['name' ]} [/bold]" )
1287- self .console .print ("[dim]Press Enter to keep current value[/dim]" )
1378+ self .console .print (
1379+ "[dim]Press Enter to keep current value. For full URLs, leave port empty.[/dim]"
1380+ )
12881381 self .console .print ()
12891382
12901383 new_name = Prompt .ask ("Name" , default = remote ["name" ]).strip ()
1291- new_host = Prompt .ask ("Host" , default = remote .get ("host" , "" )).strip ()
1292- new_port_str = Prompt .ask ("Port" , default = str (remote .get ("port" , 8000 ))).strip ()
1293- try :
1294- new_port = int (new_port_str )
1295- except ValueError :
1296- new_port = remote .get ("port" , 8000 )
1384+ new_host = Prompt .ask (
1385+ "Host (or full URL)" , default = remote .get ("host" , "" )
1386+ ).strip ()
1387+
1388+ # Get current port, handle empty string
1389+ current_port = remote .get ("port" , "" )
1390+ port_default = str (current_port ) if current_port != "" else ""
1391+
1392+ new_port_str = Prompt .ask (
1393+ "Port (empty for full URLs)" , default = port_default
1394+ ).strip ()
1395+ if new_port_str == "" :
1396+ new_port = ""
1397+ else :
1398+ try :
1399+ new_port = int (new_port_str )
1400+ except ValueError :
1401+ new_port = current_port if current_port != "" else 8000
12971402
12981403 current_key = remote .get ("api_key" , "" ) or ""
12991404 display_key = f"{ current_key [:8 ]} ..." if len (current_key ) > 8 else current_key
0 commit comments