Mirrowel commited on
Commit
1ce8eba
·
1 Parent(s): c264be0

refactor(ui): 🔨 replace console.clear with cross-platform clear_screen function

Browse files

Replaced all instances of `console.clear()` with a new `clear_screen()` helper function that uses native OS commands (`cls` for Windows, `clear` for Unix-like systems) instead of ANSI escape sequences.

- Adds `clear_screen()` function to launcher_tui.py, settings_tool.py, and credential_tool.py
- Replaces 18 instances of `console.clear()` across the codebase
- Improves terminal clearing reliability on classic Windows conhost and modern terminals (Windows Terminal, Linux, Mac)
- Removes unused anthropic_provider.py and bedrock_provider.py files
- Enhances credential_tool API key setup with better provider filtering logic to prevent duplicates
- Adds debug mode to show environment variable names in credential tool

src/proxy_app/launcher_tui.py CHANGED
@@ -16,6 +16,17 @@ from dotenv import load_dotenv, set_key
16
  console = Console()
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
19
  class LauncherConfig:
20
  """Manages launcher_config.json (host, port, logging only)"""
21
 
@@ -262,7 +273,7 @@ class LauncherTUI:
262
 
263
  def show_main_menu(self):
264
  """Display main menu and handle selection"""
265
- self.console.clear()
266
 
267
  # Detect all settings
268
  settings = SettingsDetector.get_all_settings()
@@ -394,7 +405,7 @@ class LauncherTUI:
394
  def show_config_menu(self):
395
  """Display configuration sub-menu"""
396
  while True:
397
- self.console.clear()
398
 
399
  self.console.print(Panel.fit(
400
  "[bold cyan]⚙️ Proxy Configuration[/bold cyan]",
@@ -455,7 +466,7 @@ class LauncherTUI:
455
 
456
  def show_provider_settings_menu(self):
457
  """Display provider/advanced settings (read-only + launch tool)"""
458
- self.console.clear()
459
 
460
  settings = SettingsDetector.get_all_settings()
461
  credentials = settings["credentials"]
@@ -573,7 +584,7 @@ class LauncherTUI:
573
  import time
574
 
575
  # CRITICAL: Show full loading UI to replace the 6-7 second blank wait
576
- self.console.clear()
577
 
578
  _start_time = time.time()
579
 
@@ -610,7 +621,7 @@ class LauncherTUI:
610
 
611
  def show_about(self):
612
  """Display About page with project information"""
613
- self.console.clear()
614
 
615
  self.console.print(Panel.fit(
616
  "[bold cyan]ℹ️ About LLM API Key Proxy[/bold cyan]",
@@ -654,7 +665,7 @@ class LauncherTUI:
654
  """Prepare and launch proxy in same window"""
655
  # Check if forced onboarding needed
656
  if self.needs_onboarding():
657
- self.console.clear()
658
  self.console.print(Panel(
659
  Text.from_markup(
660
  "⚠️ [bold yellow]Setup Required[/bold yellow]\n\n"
@@ -677,13 +688,13 @@ class LauncherTUI:
677
  return
678
 
679
  # Clear console and modify sys.argv
680
- self.console.clear()
681
  self.console.print(f"\n[bold green]🚀 Starting proxy on {self.config.config['host']}:{self.config.config['port']}...[/bold green]\n")
682
 
683
  # Clear console again to remove the starting message before main.py shows loading details
684
  import time
685
  time.sleep(0.5) # Brief pause so user sees the message
686
- self.console.clear()
687
 
688
  # Reconstruct sys.argv for main.py
689
  sys.argv = [
 
16
  console = Console()
17
 
18
 
19
+ def clear_screen():
20
+ """
21
+ Cross-platform terminal clear that works robustly on both
22
+ classic Windows conhost and modern terminals (Windows Terminal, Linux, Mac).
23
+
24
+ Uses native OS commands instead of ANSI escape sequences:
25
+ - Windows (conhost & Windows Terminal): cls
26
+ - Unix-like systems (Linux, Mac): clear
27
+ """
28
+ os.system('cls' if os.name == 'nt' else 'clear')
29
+
30
  class LauncherConfig:
31
  """Manages launcher_config.json (host, port, logging only)"""
32
 
 
273
 
274
  def show_main_menu(self):
275
  """Display main menu and handle selection"""
276
+ clear_screen()
277
 
278
  # Detect all settings
279
  settings = SettingsDetector.get_all_settings()
 
405
  def show_config_menu(self):
406
  """Display configuration sub-menu"""
407
  while True:
408
+ clear_screen()
409
 
410
  self.console.print(Panel.fit(
411
  "[bold cyan]⚙️ Proxy Configuration[/bold cyan]",
 
466
 
467
  def show_provider_settings_menu(self):
468
  """Display provider/advanced settings (read-only + launch tool)"""
469
+ clear_screen()
470
 
471
  settings = SettingsDetector.get_all_settings()
472
  credentials = settings["credentials"]
 
584
  import time
585
 
586
  # CRITICAL: Show full loading UI to replace the 6-7 second blank wait
587
+ clear_screen()
588
 
589
  _start_time = time.time()
590
 
 
621
 
622
  def show_about(self):
623
  """Display About page with project information"""
624
+ clear_screen()
625
 
626
  self.console.print(Panel.fit(
627
  "[bold cyan]ℹ️ About LLM API Key Proxy[/bold cyan]",
 
665
  """Prepare and launch proxy in same window"""
666
  # Check if forced onboarding needed
667
  if self.needs_onboarding():
668
+ clear_screen()
669
  self.console.print(Panel(
670
  Text.from_markup(
671
  "⚠️ [bold yellow]Setup Required[/bold yellow]\n\n"
 
688
  return
689
 
690
  # Clear console and modify sys.argv
691
+ clear_screen()
692
  self.console.print(f"\n[bold green]🚀 Starting proxy on {self.config.config['host']}:{self.config.config['port']}...[/bold green]\n")
693
 
694
  # Clear console again to remove the starting message before main.py shows loading details
695
  import time
696
  time.sleep(0.5) # Brief pause so user sees the message
697
+ clear_screen()
698
 
699
  # Reconstruct sys.argv for main.py
700
  sys.argv = [
src/proxy_app/main.py CHANGED
@@ -1137,7 +1137,7 @@ if __name__ == "__main__":
1137
 
1138
  def show_onboarding_message():
1139
  """Display clear explanatory message for why onboarding is needed."""
1140
- console.clear() # Clear terminal for clean presentation
1141
  console.print(Panel.fit(
1142
  "[bold cyan]🚀 LLM API Key Proxy - First Time Setup[/bold cyan]",
1143
  border_style="cyan"
 
1137
 
1138
  def show_onboarding_message():
1139
  """Display clear explanatory message for why onboarding is needed."""
1140
+ os.system('cls' if os.name == 'nt' else 'clear') # Clear terminal for clean presentation
1141
  console.print(Panel.fit(
1142
  "[bold cyan]🚀 LLM API Key Proxy - First Time Setup[/bold cyan]",
1143
  border_style="cyan"
src/proxy_app/settings_tool.py CHANGED
@@ -15,6 +15,18 @@ from dotenv import set_key, unset_key
15
  console = Console()
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  class AdvancedSettings:
19
  """Manages pending changes to .env"""
20
 
@@ -389,7 +401,7 @@ class SettingsTool:
389
 
390
  def show_main_menu(self):
391
  """Display settings categories"""
392
- self.console.clear()
393
 
394
  self.console.print(Panel.fit(
395
  "[bold cyan]🔧 Advanced Settings Configuration[/bold cyan]",
@@ -436,7 +448,7 @@ class SettingsTool:
436
  def manage_custom_providers(self):
437
  """Manage custom provider API bases"""
438
  while True:
439
- self.console.clear()
440
 
441
  providers = self.provider_mgr.get_current_providers()
442
 
@@ -533,7 +545,7 @@ class SettingsTool:
533
  def manage_model_definitions(self):
534
  """Manage provider model definitions"""
535
  while True:
536
- self.console.clear()
537
 
538
  all_providers = self.model_mgr.get_all_providers_with_models()
539
 
@@ -710,7 +722,7 @@ class SettingsTool:
710
  current_models = {m: {} for m in current_models}
711
 
712
  while True:
713
- self.console.clear()
714
  self.console.print(f"[bold]Editing models for: {provider}[/bold]\n")
715
  self.console.print("Current models:")
716
  for i, (name, definition) in enumerate(current_models.items(), 1):
@@ -788,7 +800,7 @@ class SettingsTool:
788
  input("\nPress Enter to continue...")
789
  return
790
 
791
- self.console.clear()
792
  self.console.print(f"[bold]Provider: {provider}[/bold]\n")
793
  self.console.print("[bold]📦 Configured Models:[/bold]")
794
  self.console.print("━" * 50)
@@ -816,7 +828,7 @@ class SettingsTool:
816
  def manage_provider_settings(self):
817
  """Manage provider-specific settings (Antigravity, Gemini CLI)"""
818
  while True:
819
- self.console.clear()
820
 
821
  available_providers = self.provider_settings_mgr.get_available_providers()
822
 
@@ -863,7 +875,7 @@ class SettingsTool:
863
  def _manage_single_provider_settings(self, provider: str):
864
  """Manage settings for a single provider"""
865
  while True:
866
- self.console.clear()
867
 
868
  display_name = provider.replace("_", " ").title()
869
  definitions = self.provider_settings_mgr.get_provider_settings_definitions(provider)
@@ -1005,7 +1017,7 @@ class SettingsTool:
1005
  def manage_concurrency_limits(self):
1006
  """Manage concurrency limits"""
1007
  while True:
1008
- self.console.clear()
1009
 
1010
  limits = self.concurrency_mgr.get_current_limits()
1011
 
 
15
  console = Console()
16
 
17
 
18
+ def clear_screen():
19
+ """
20
+ Cross-platform terminal clear that works robustly on both
21
+ classic Windows conhost and modern terminals (Windows Terminal, Linux, Mac).
22
+
23
+ Uses native OS commands instead of ANSI escape sequences:
24
+ - Windows (conhost & Windows Terminal): cls
25
+ - Unix-like systems (Linux, Mac): clear
26
+ """
27
+ os.system('cls' if os.name == 'nt' else 'clear')
28
+
29
+
30
  class AdvancedSettings:
31
  """Manages pending changes to .env"""
32
 
 
401
 
402
  def show_main_menu(self):
403
  """Display settings categories"""
404
+ clear_screen()
405
 
406
  self.console.print(Panel.fit(
407
  "[bold cyan]🔧 Advanced Settings Configuration[/bold cyan]",
 
448
  def manage_custom_providers(self):
449
  """Manage custom provider API bases"""
450
  while True:
451
+ clear_screen()
452
 
453
  providers = self.provider_mgr.get_current_providers()
454
 
 
545
  def manage_model_definitions(self):
546
  """Manage provider model definitions"""
547
  while True:
548
+ clear_screen()
549
 
550
  all_providers = self.model_mgr.get_all_providers_with_models()
551
 
 
722
  current_models = {m: {} for m in current_models}
723
 
724
  while True:
725
+ clear_screen()
726
  self.console.print(f"[bold]Editing models for: {provider}[/bold]\n")
727
  self.console.print("Current models:")
728
  for i, (name, definition) in enumerate(current_models.items(), 1):
 
800
  input("\nPress Enter to continue...")
801
  return
802
 
803
+ clear_screen()
804
  self.console.print(f"[bold]Provider: {provider}[/bold]\n")
805
  self.console.print("[bold]📦 Configured Models:[/bold]")
806
  self.console.print("━" * 50)
 
828
  def manage_provider_settings(self):
829
  """Manage provider-specific settings (Antigravity, Gemini CLI)"""
830
  while True:
831
+ clear_screen()
832
 
833
  available_providers = self.provider_settings_mgr.get_available_providers()
834
 
 
875
  def _manage_single_provider_settings(self, provider: str):
876
  """Manage settings for a single provider"""
877
  while True:
878
+ clear_screen()
879
 
880
  display_name = provider.replace("_", " ").title()
881
  definitions = self.provider_settings_mgr.get_provider_settings_definitions(provider)
 
1017
  def manage_concurrency_limits(self):
1018
  """Manage concurrency limits"""
1019
  while True:
1020
+ clear_screen()
1021
 
1022
  limits = self.concurrency_mgr.get_current_limits()
1023
 
src/rotator_library/credential_tool.py CHANGED
@@ -37,6 +37,18 @@ def _ensure_providers_loaded():
37
  return _provider_factory, _provider_plugins
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def _get_credential_number_from_filename(filename: str) -> int:
41
  """
42
  Extract credential number from filename like 'provider_oauth_1.json' -> 1
@@ -127,6 +139,9 @@ async def setup_api_key():
127
  """
128
  console.print(Panel("[bold cyan]API Key Setup[/bold cyan]", expand=False))
129
 
 
 
 
130
  # Verified list of LiteLLM providers with their friendly names and API key variables
131
  LITELLM_PROVIDERS = {
132
  "OpenAI": "OPENAI_API_KEY", "Anthropic": "ANTHROPIC_API_KEY",
@@ -162,26 +177,59 @@ async def setup_api_key():
162
  "Nscale": "NSCALE_API_KEY", "Recraft": "RECRAFT_API_KEY",
163
  "v0": "V0_API_KEY", "Vercel": "VERCEL_AI_GATEWAY_API_KEY",
164
  "Topaz": "TOPAZ_API_KEY", "ElevenLabs": "ELEVENLABS_API_KEY",
165
- "Deepgram": "DEEPGRAM_API_KEY", "Custom API": "CUSTOM_API_KEY",
166
  "GitHub Models": "GITHUB_TOKEN", "GitHub Copilot": "GITHUB_COPILOT_API_KEY",
167
  }
168
 
169
  # Discover custom providers and add them to the list
170
- # Note: gemini_cli is OAuth-only, but qwen_code and iflow support both OAuth and API keys
 
 
171
  _, PROVIDER_PLUGINS = _ensure_providers_loaded()
172
- oauth_only_providers = {'gemini_cli', 'antigravity'}
173
- discovered_providers = {
174
- p.replace('_', ' ').title(): p.upper() + "_API_KEY"
175
- for p in PROVIDER_PLUGINS.keys()
176
- if p not in oauth_only_providers and p.replace('_', ' ').title() not in LITELLM_PROVIDERS
 
 
 
 
 
 
177
  }
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  combined_providers = {**LITELLM_PROVIDERS, **discovered_providers}
180
  provider_display_list = sorted(combined_providers.keys())
181
 
182
  provider_text = Text()
183
  for i, provider_name in enumerate(provider_display_list):
184
- provider_text.append(f" {i + 1}. {provider_name}\n")
 
 
 
 
 
 
185
 
186
  console.print(Panel(provider_text, title="Available Providers for API Key", style="bold blue"))
187
 
@@ -1000,7 +1048,7 @@ async def export_credentials_submenu():
1000
  Submenu for credential export options.
1001
  """
1002
  while True:
1003
- console.clear()
1004
  console.print(Panel("[bold cyan]Export Credentials to .env[/bold cyan]", title="--- API Key Proxy ---", expand=False))
1005
 
1006
  console.print(Panel(
@@ -1111,7 +1159,7 @@ async def main(clear_on_start=True):
1111
 
1112
  while True:
1113
  # Clear screen between menu selections for cleaner UX
1114
- console.clear()
1115
  console.print(Panel("[bold cyan]Interactive Credential Setup[/bold cyan]", title="--- API Key Proxy ---", expand=False))
1116
 
1117
  console.print(Panel(
@@ -1179,7 +1227,7 @@ async def main(clear_on_start=True):
1179
  elif setup_type == "2":
1180
  await setup_api_key()
1181
  #console.print("\n[dim]Press Enter to return to main menu...[/dim]")
1182
- input()
1183
 
1184
  elif setup_type == "3":
1185
  await export_credentials_submenu()
@@ -1225,7 +1273,7 @@ def run_credential_tool(from_launcher=False):
1225
  # If from launcher, don't clear screen at start to preserve loading messages
1226
  try:
1227
  asyncio.run(main(clear_on_start=not from_launcher))
1228
- console.clear() # Clear terminal when credential tool exits
1229
  except KeyboardInterrupt:
1230
  console.print("\n[bold yellow]Exiting setup.[/bold yellow]")
1231
- console.clear() # Clear terminal on keyboard interrupt too
 
37
  return _provider_factory, _provider_plugins
38
 
39
 
40
+ def clear_screen():
41
+ """
42
+ Cross-platform terminal clear that works robustly on both
43
+ classic Windows conhost and modern terminals (Windows Terminal, Linux, Mac).
44
+
45
+ Uses native OS commands instead of ANSI escape sequences:
46
+ - Windows (conhost & Windows Terminal): cls
47
+ - Unix-like systems (Linux, Mac): clear
48
+ """
49
+ os.system('cls' if os.name == 'nt' else 'clear')
50
+
51
+
52
  def _get_credential_number_from_filename(filename: str) -> int:
53
  """
54
  Extract credential number from filename like 'provider_oauth_1.json' -> 1
 
139
  """
140
  console.print(Panel("[bold cyan]API Key Setup[/bold cyan]", expand=False))
141
 
142
+ # Debug toggle: Set to True to see env var names next to each provider
143
+ SHOW_ENV_VAR_NAMES = True
144
+
145
  # Verified list of LiteLLM providers with their friendly names and API key variables
146
  LITELLM_PROVIDERS = {
147
  "OpenAI": "OPENAI_API_KEY", "Anthropic": "ANTHROPIC_API_KEY",
 
177
  "Nscale": "NSCALE_API_KEY", "Recraft": "RECRAFT_API_KEY",
178
  "v0": "V0_API_KEY", "Vercel": "VERCEL_AI_GATEWAY_API_KEY",
179
  "Topaz": "TOPAZ_API_KEY", "ElevenLabs": "ELEVENLABS_API_KEY",
180
+ "Deepgram": "DEEPGRAM_API_KEY",
181
  "GitHub Models": "GITHUB_TOKEN", "GitHub Copilot": "GITHUB_COPILOT_API_KEY",
182
  }
183
 
184
  # Discover custom providers and add them to the list
185
+ # Note: gemini_cli and antigravity are OAuth-only
186
+ # qwen_code API key support is a fallback
187
+ # iflow API key support is a feature
188
  _, PROVIDER_PLUGINS = _ensure_providers_loaded()
189
+
190
+ # Build a set of environment variables already in LITELLM_PROVIDERS
191
+ # to avoid duplicates based on the actual API key names
192
+ litellm_env_vars = set(LITELLM_PROVIDERS.values())
193
+
194
+ # Providers to exclude from API key list
195
+ exclude_providers = {
196
+ 'gemini_cli', # OAuth-only
197
+ 'antigravity', # OAuth-only
198
+ 'qwen_code', # API key is fallback, OAuth is primary - don't advertise
199
+ 'openai_compatible', # Base class, not a real provider
200
  }
201
 
202
+ discovered_providers = {}
203
+ for provider_key in PROVIDER_PLUGINS.keys():
204
+ if provider_key in exclude_providers:
205
+ continue
206
+
207
+ # Create environment variable name
208
+ env_var = provider_key.upper() + "_API_KEY"
209
+
210
+ # Check if this env var already exists in LITELLM_PROVIDERS
211
+ # This catches duplicates like GEMINI_API_KEY, MISTRAL_API_KEY, etc.
212
+ if env_var in litellm_env_vars:
213
+ # Already in LITELLM_PROVIDERS with better name, skip this one
214
+ continue
215
+
216
+ # Create display name for this custom provider
217
+ display_name = provider_key.replace('_', ' ').title()
218
+ discovered_providers[display_name] = env_var
219
+
220
+ # LITELLM_PROVIDERS takes precedence (comes first in merge)
221
  combined_providers = {**LITELLM_PROVIDERS, **discovered_providers}
222
  provider_display_list = sorted(combined_providers.keys())
223
 
224
  provider_text = Text()
225
  for i, provider_name in enumerate(provider_display_list):
226
+ if SHOW_ENV_VAR_NAMES:
227
+ # Extract env var prefix (before _API_KEY)
228
+ env_var = combined_providers[provider_name]
229
+ prefix = env_var.replace("_API_KEY", "").replace("_", " ")
230
+ provider_text.append(f" {i + 1}. {provider_name} ({prefix})\n")
231
+ else:
232
+ provider_text.append(f" {i + 1}. {provider_name}\n")
233
 
234
  console.print(Panel(provider_text, title="Available Providers for API Key", style="bold blue"))
235
 
 
1048
  Submenu for credential export options.
1049
  """
1050
  while True:
1051
+ clear_screen()
1052
  console.print(Panel("[bold cyan]Export Credentials to .env[/bold cyan]", title="--- API Key Proxy ---", expand=False))
1053
 
1054
  console.print(Panel(
 
1159
 
1160
  while True:
1161
  # Clear screen between menu selections for cleaner UX
1162
+ clear_screen()
1163
  console.print(Panel("[bold cyan]Interactive Credential Setup[/bold cyan]", title="--- API Key Proxy ---", expand=False))
1164
 
1165
  console.print(Panel(
 
1227
  elif setup_type == "2":
1228
  await setup_api_key()
1229
  #console.print("\n[dim]Press Enter to return to main menu...[/dim]")
1230
+ #input()
1231
 
1232
  elif setup_type == "3":
1233
  await export_credentials_submenu()
 
1273
  # If from launcher, don't clear screen at start to preserve loading messages
1274
  try:
1275
  asyncio.run(main(clear_on_start=not from_launcher))
1276
+ clear_screen() # Clear terminal when credential tool exits
1277
  except KeyboardInterrupt:
1278
  console.print("\n[bold yellow]Exiting setup.[/bold yellow]")
1279
+ clear_screen() # Clear terminal on keyboard interrupt too
src/rotator_library/providers/anthropic_provider.py DELETED
@@ -1,31 +0,0 @@
1
- import httpx
2
- import logging
3
- from typing import List
4
- from .provider_interface import ProviderInterface
5
-
6
- lib_logger = logging.getLogger('rotator_library')
7
- lib_logger.propagate = False # Ensure this logger doesn't propagate to root
8
- if not lib_logger.handlers:
9
- lib_logger.addHandler(logging.NullHandler())
10
-
11
- class AnthropicProvider(ProviderInterface):
12
- """
13
- Provider implementation for the Anthropic API.
14
- """
15
- async def get_models(self, api_key: str, client: httpx.AsyncClient) -> List[str]:
16
- """
17
- Fetches the list of available models from the Anthropic API.
18
- """
19
- try:
20
- response = await client.get(
21
- "https://api.anthropic.com/v1/models",
22
- headers={
23
- "x-api-key": api_key,
24
- "anthropic-version": "2023-06-01"
25
- }
26
- )
27
- response.raise_for_status()
28
- return [f"anthropic/{model['id']}" for model in response.json().get("data", [])]
29
- except httpx.RequestError as e:
30
- lib_logger.error(f"Failed to fetch Anthropic models: {e}")
31
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/rotator_library/providers/bedrock_provider.py DELETED
@@ -1,29 +0,0 @@
1
- import httpx
2
- import logging
3
- from typing import List
4
- from .provider_interface import ProviderInterface
5
-
6
- lib_logger = logging.getLogger('rotator_library')
7
- lib_logger.propagate = False # Ensure this logger doesn't propagate to root
8
- if not lib_logger.handlers:
9
- lib_logger.addHandler(logging.NullHandler())
10
-
11
- class BedrockProvider(ProviderInterface):
12
- """
13
- Provider implementation for AWS Bedrock.
14
- """
15
- async def get_models(self, api_key: str, client: httpx.AsyncClient) -> List[str]:
16
- """
17
- Returns a hardcoded list of common Bedrock models, as there is no
18
- simple, unauthenticated API endpoint to list them.
19
- """
20
- # Note: Listing Bedrock models typically requires AWS credentials and boto3.
21
- # For a simple, key-based proxy, we'll list common models.
22
- # This can be expanded with full AWS authentication if needed.
23
- lib_logger.info("Returning hardcoded list for Bedrock. Full discovery requires AWS auth.")
24
- return [
25
- "bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
26
- "bedrock/anthropic.claude-3-haiku-20240307-v1:0",
27
- "bedrock/cohere.command-r-plus-v1:0",
28
- "bedrock/mistral.mistral-large-2402-v1:0",
29
- ]