Skip to content

Commit 4aa703f

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feature/anthropic-endpoints
2 parents b7b5d07 + f421da8 commit 4aa703f

File tree

2 files changed

+140
-26
lines changed

2 files changed

+140
-26
lines changed

src/proxy_app/quota_viewer.py

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
154159
def 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

src/proxy_app/quota_viewer_config.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import json
1111
from pathlib import Path
12-
from typing import Any, Dict, List, Optional
12+
from typing import Any, Dict, List, Optional, Union
1313

1414

1515
class QuotaViewerConfig:
@@ -109,7 +109,7 @@ def add_remote(
109109
self,
110110
name: str,
111111
host: str,
112-
port: int = 8000,
112+
port: Optional[Union[int, str]] = 8000,
113113
api_key: Optional[str] = None,
114114
is_default: bool = False,
115115
) -> bool:
@@ -118,8 +118,8 @@ def add_remote(
118118
119119
Args:
120120
name: Display name for the remote
121-
host: Hostname or IP address
122-
port: Port number (default 8000)
121+
host: Hostname, IP address, or full URL (e.g., https://api.example.com/v1)
122+
port: Port number (default 8000). Can be None or empty string for full URLs.
123123
api_key: Optional API key for authentication
124124
is_default: Whether this should be the default remote
125125
@@ -135,10 +135,18 @@ def add_remote(
135135
for remote in self.config.get("remotes", []):
136136
remote["is_default"] = False
137137

138+
# Normalize port - allow empty/None for full URL hosts
139+
if port == "" or port is None:
140+
normalized_port = ""
141+
else:
142+
normalized_port = (
143+
int(port) if isinstance(port, str) and port.isdigit() else port
144+
)
145+
138146
remote = {
139147
"name": name,
140148
"host": host,
141-
"port": port,
149+
"port": normalized_port,
142150
"api_key": api_key,
143151
"is_default": is_default,
144152
}
@@ -152,6 +160,7 @@ def update_remote(self, name: str, **kwargs) -> bool:
152160
Args:
153161
name: Name of the remote to update
154162
**kwargs: Fields to update (host, port, api_key, is_default, new_name)
163+
port can be int, str, or empty string for full URL hosts
155164
156165
Returns:
157166
True on success, False if remote not found

0 commit comments

Comments
 (0)