Mirrowel commited on
Commit
cfa8697
·
2 Parent(s): 475234cb7df2fe

Merge remote-tracking branch 'origin/feature/filtering-tool' into dev

Browse files
DOCUMENTATION.md CHANGED
@@ -10,6 +10,7 @@ The project is a monorepo containing two primary components:
10
  * **Batch Manager**: Optimizes high-volume embedding requests.
11
  * **Detailed Logger**: Provides per-request file logging for debugging.
12
  * **OpenAI-Compatible Endpoints**: `/v1/chat/completions`, `/v1/embeddings`, etc.
 
13
  2. **The Resilience Library (`rotator_library`)**: This is the core engine that provides high availability. It is consumed by the proxy app to manage a pool of API keys, handle errors gracefully, and ensure requests are completed successfully even when individual keys or provider endpoints face issues.
14
 
15
  This architecture cleanly separates the API interface from the resilience logic, making the library a portable and powerful tool for any application needing robust API key management.
@@ -1452,3 +1453,83 @@ stats = cache.get_stats()
1452
  # Includes: {"disk_available": True, "disk_errors": 0, ...}
1453
  ```
1454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  * **Batch Manager**: Optimizes high-volume embedding requests.
11
  * **Detailed Logger**: Provides per-request file logging for debugging.
12
  * **OpenAI-Compatible Endpoints**: `/v1/chat/completions`, `/v1/embeddings`, etc.
13
+ * **Model Filter GUI**: Visual interface for configuring model ignore/whitelist rules per provider (see Section 6).
14
  2. **The Resilience Library (`rotator_library`)**: This is the core engine that provides high availability. It is consumed by the proxy app to manage a pool of API keys, handle errors gracefully, and ensure requests are completed successfully even when individual keys or provider endpoints face issues.
15
 
16
  This architecture cleanly separates the API interface from the resilience logic, making the library a portable and powerful tool for any application needing robust API key management.
 
1453
  # Includes: {"disk_available": True, "disk_errors": 0, ...}
1454
  ```
1455
 
1456
+ ---
1457
+
1458
+ ## 6. Model Filter GUI
1459
+
1460
+ The Model Filter GUI (`model_filter_gui.py`) provides a visual interface for configuring model ignore and whitelist rules per provider. It replaces the need to manually edit `IGNORE_MODELS_*` and `WHITELIST_MODELS_*` environment variables.
1461
+
1462
+ ### 6.1. Overview
1463
+
1464
+ **Purpose**: Visually manage which models are exposed via the `/v1/models` endpoint for each provider.
1465
+
1466
+ **Launch**:
1467
+ ```bash
1468
+ python -c "from src.proxy_app.model_filter_gui import run_model_filter_gui; run_model_filter_gui()"
1469
+ ```
1470
+
1471
+ Or via the launcher TUI if integrated.
1472
+
1473
+ ### 6.2. Features
1474
+
1475
+ #### Core Functionality
1476
+
1477
+ - **Provider Selection**: Dropdown to switch between available providers with automatic model fetching
1478
+ - **Ignore Rules**: Pattern-based rules (supports wildcards like `*-preview`, `gpt-4*`) to exclude models
1479
+ - **Whitelist Rules**: Pattern-based rules to explicitly include models, overriding ignore rules
1480
+ - **Real-time Preview**: Typing in rule input fields highlights affected models before committing
1481
+ - **Rule-Model Linking**: Click a model to highlight the affecting rule; click a rule to highlight all affected models
1482
+ - **Persistence**: Rules saved to `.env` file in standard `IGNORE_MODELS_<PROVIDER>` and `WHITELIST_MODELS_<PROVIDER>` format
1483
+
1484
+ #### Dual-Pane Model View
1485
+
1486
+ The interface displays two synchronized lists:
1487
+
1488
+ | Left Pane | Right Pane |
1489
+ |-----------|------------|
1490
+ | All fetched models (plain text) | Same models with color-coded status |
1491
+ | Shows total count | Shows available/ignored count |
1492
+ | Scrolls in sync with right pane | Color indicates affecting rule |
1493
+
1494
+ **Color Coding**:
1495
+ - **Green**: Model is available (no rule affects it, or whitelisted)
1496
+ - **Red/Orange tones**: Model is ignored (color matches the specific ignore rule)
1497
+ - **Blue/Teal tones**: Model is explicitly whitelisted (color matches the whitelist rule)
1498
+
1499
+ #### Rule Management
1500
+
1501
+ - **Comma-separated input**: Add multiple rules at once (e.g., `*-preview, *-beta, gpt-3.5*`)
1502
+ - **Wildcard support**: `*` matches any characters (e.g., `gemini-*-preview`)
1503
+ - **Affected count**: Each rule shows how many models it affects
1504
+ - **Tooltips**: Hover over a rule to see the list of affected models
1505
+ - **Instant delete**: Click the × button to remove a rule immediately
1506
+
1507
+ ### 6.3. Keyboard Shortcuts
1508
+
1509
+ | Shortcut | Action |
1510
+ |----------|--------|
1511
+ | `Ctrl+S` | Save changes to `.env` |
1512
+ | `Ctrl+R` | Refresh models from provider |
1513
+ | `Ctrl+F` | Focus search field |
1514
+ | `F1` | Show help dialog |
1515
+ | `Escape` | Clear search / Clear highlights |
1516
+
1517
+ ### 6.4. Context Menu
1518
+
1519
+ Right-click on any model to access:
1520
+
1521
+ - **Add to Ignore List**: Creates an ignore rule for the exact model name
1522
+ - **Add to Whitelist**: Creates a whitelist rule for the exact model name
1523
+ - **View Affecting Rule**: Highlights the rule that affects this model
1524
+ - **Copy Model Name**: Copies the full model ID to clipboard
1525
+
1526
+ ### 6.5. Integration with Proxy
1527
+
1528
+ The GUI modifies the same environment variables that the `RotatingClient` reads:
1529
+
1530
+ 1. **GUI saves rules** → Updates `.env` file
1531
+ 2. **Proxy reads on startup** → Loads `IGNORE_MODELS_*` and `WHITELIST_MODELS_*`
1532
+ 3. **Proxy applies rules** → `get_available_models()` filters based on rules
1533
+
1534
+ **Note**: The proxy must be restarted to pick up rule changes made via the GUI (or use the Launcher TUI's reload functionality if available).
1535
+
requirements.txt CHANGED
@@ -19,3 +19,9 @@ aiohttp
19
  colorlog
20
 
21
  rich
 
 
 
 
 
 
 
19
  colorlog
20
 
21
  rich
22
+
23
+ # GUI for model filter configuration
24
+ customtkinter
25
+
26
+ # For building the executable
27
+ pyinstaller
src/proxy_app/build.py CHANGED
@@ -3,6 +3,7 @@ import sys
3
  import platform
4
  import subprocess
5
 
 
6
  def get_providers():
7
  """
8
  Scans the 'src/rotator_library/providers' directory to find all provider modules.
@@ -24,6 +25,7 @@ def get_providers():
24
  hidden_imports.append(f"--hidden-import={module_name}")
25
  return hidden_imports
26
 
 
27
  def main():
28
  """
29
  Constructs and runs the PyInstaller command to build the executable.
@@ -47,22 +49,27 @@ def main():
47
  "--collect-data",
48
  "litellm",
49
  # Optimization: Exclude unused heavy modules
50
- "--exclude-module=tkinter",
51
  "--exclude-module=matplotlib",
52
  "--exclude-module=IPython",
53
  "--exclude-module=jupyter",
54
  "--exclude-module=notebook",
55
  "--exclude-module=PIL.ImageTk",
56
  # Optimization: Enable UPX compression (if available)
57
- "--upx-dir=upx" if platform.system() != "Darwin" else "--noupx", # macOS has issues with UPX
 
 
58
  # Optimization: Strip debug symbols (smaller binary)
59
- "--strip" if platform.system() != "Windows" else "--console", # Windows gets clean console
 
 
60
  ]
61
 
62
  # Add hidden imports for providers
63
  provider_imports = get_providers()
64
  if not provider_imports:
65
- print("Warning: No providers found. The build might not include any LLM providers.")
 
 
66
  command.extend(provider_imports)
67
 
68
  # Add the main script
@@ -80,5 +87,6 @@ def main():
80
  except FileNotFoundError:
81
  print("Error: PyInstaller is not installed or not in the system's PATH.")
82
 
 
83
  if __name__ == "__main__":
84
  main()
 
3
  import platform
4
  import subprocess
5
 
6
+
7
  def get_providers():
8
  """
9
  Scans the 'src/rotator_library/providers' directory to find all provider modules.
 
25
  hidden_imports.append(f"--hidden-import={module_name}")
26
  return hidden_imports
27
 
28
+
29
  def main():
30
  """
31
  Constructs and runs the PyInstaller command to build the executable.
 
49
  "--collect-data",
50
  "litellm",
51
  # Optimization: Exclude unused heavy modules
 
52
  "--exclude-module=matplotlib",
53
  "--exclude-module=IPython",
54
  "--exclude-module=jupyter",
55
  "--exclude-module=notebook",
56
  "--exclude-module=PIL.ImageTk",
57
  # Optimization: Enable UPX compression (if available)
58
+ "--upx-dir=upx"
59
+ if platform.system() != "Darwin"
60
+ else "--noupx", # macOS has issues with UPX
61
  # Optimization: Strip debug symbols (smaller binary)
62
+ "--strip"
63
+ if platform.system() != "Windows"
64
+ else "--console", # Windows gets clean console
65
  ]
66
 
67
  # Add hidden imports for providers
68
  provider_imports = get_providers()
69
  if not provider_imports:
70
+ print(
71
+ "Warning: No providers found. The build might not include any LLM providers."
72
+ )
73
  command.extend(provider_imports)
74
 
75
  # Add the main script
 
87
  except FileNotFoundError:
88
  print("Error: PyInstaller is not installed or not in the system's PATH.")
89
 
90
+
91
  if __name__ == "__main__":
92
  main()
src/proxy_app/model_filter_gui.py ADDED
The diff for this file is too large to render. See raw diff
 
src/proxy_app/settings_tool.py CHANGED
@@ -749,23 +749,20 @@ class SettingsTool:
749
  self.console.print(" 3. ⚡ Concurrency Limits")
750
  self.console.print(" 4. 🔄 Rotation Modes")
751
  self.console.print(" 5. 🔬 Provider-Specific Settings")
752
- self.console.print(" 6. 💾 Save & Exit")
753
- self.console.print(" 7. 🚫 Exit Without Saving")
 
754
 
755
  self.console.print()
756
  self.console.print("━" * 70)
757
 
758
  self.console.print(self._get_pending_status_text())
759
 
760
- self.console.print()
761
- self.console.print(
762
- "[dim]⚠️ Model filters not supported - edit .env for IGNORE_MODELS_* / WHITELIST_MODELS_*[/dim]"
763
- )
764
  self.console.print()
765
 
766
  choice = Prompt.ask(
767
  "Select option",
768
- choices=["1", "2", "3", "4", "5", "6", "7"],
769
  show_choices=False,
770
  )
771
 
@@ -780,8 +777,10 @@ class SettingsTool:
780
  elif choice == "5":
781
  self.manage_provider_settings()
782
  elif choice == "6":
783
- self.save_and_exit()
784
  elif choice == "7":
 
 
785
  self.exit_without_saving()
786
 
787
  def manage_custom_providers(self):
@@ -1393,6 +1392,28 @@ class SettingsTool:
1393
 
1394
  input("Press Enter to return...")
1395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1396
  def manage_provider_settings(self):
1397
  """Manage provider-specific settings (Antigravity, Gemini CLI)"""
1398
  while True:
 
749
  self.console.print(" 3. ⚡ Concurrency Limits")
750
  self.console.print(" 4. 🔄 Rotation Modes")
751
  self.console.print(" 5. 🔬 Provider-Specific Settings")
752
+ self.console.print(" 6. 🎯 Model Filters (Ignore/Whitelist)")
753
+ self.console.print(" 7. 💾 Save & Exit")
754
+ self.console.print(" 8. 🚫 Exit Without Saving")
755
 
756
  self.console.print()
757
  self.console.print("━" * 70)
758
 
759
  self.console.print(self._get_pending_status_text())
760
 
 
 
 
 
761
  self.console.print()
762
 
763
  choice = Prompt.ask(
764
  "Select option",
765
+ choices=["1", "2", "3", "4", "5", "6", "7", "8"],
766
  show_choices=False,
767
  )
768
 
 
777
  elif choice == "5":
778
  self.manage_provider_settings()
779
  elif choice == "6":
780
+ self.launch_model_filter_gui()
781
  elif choice == "7":
782
+ self.save_and_exit()
783
+ elif choice == "8":
784
  self.exit_without_saving()
785
 
786
  def manage_custom_providers(self):
 
1392
 
1393
  input("Press Enter to return...")
1394
 
1395
+ def launch_model_filter_gui(self):
1396
+ """Launch the Model Filter GUI for managing ignore/whitelist rules"""
1397
+ clear_screen()
1398
+ self.console.print("\n[cyan]Launching Model Filter GUI...[/cyan]\n")
1399
+ self.console.print(
1400
+ "[dim]The GUI will open in a separate window. Close it to return here.[/dim]\n"
1401
+ )
1402
+
1403
+ try:
1404
+ from proxy_app.model_filter_gui import run_model_filter_gui
1405
+
1406
+ run_model_filter_gui() # Blocks until GUI closes
1407
+ except ImportError as e:
1408
+ self.console.print(f"\n[red]Failed to launch Model Filter GUI: {e}[/red]")
1409
+ self.console.print()
1410
+ self.console.print(
1411
+ "[yellow]Make sure 'customtkinter' is installed:[/yellow]"
1412
+ )
1413
+ self.console.print(" [cyan]pip install customtkinter[/cyan]")
1414
+ self.console.print()
1415
+ input("Press Enter to continue...")
1416
+
1417
  def manage_provider_settings(self):
1418
  """Manage provider-specific settings (Antigravity, Gemini CLI)"""
1419
  while True:
src/rotator_library/client.py CHANGED
@@ -1,4 +1,5 @@
1
  import asyncio
 
2
  import json
3
  import re
4
  import codecs
@@ -297,7 +298,14 @@ class RotatingClient:
297
  def _is_model_ignored(self, provider: str, model_id: str) -> bool:
298
  """
299
  Checks if a model should be ignored based on the ignore list.
300
- Supports exact and partial matching for both full model IDs and model names.
 
 
 
 
 
 
 
301
  """
302
  model_provider = model_id.split("/")[0]
303
  if model_provider not in self.ignore_models:
@@ -314,52 +322,43 @@ class RotatingClient:
314
  provider_model_name = model_id
315
 
316
  for ignored_pattern in ignore_list:
317
- if ignored_pattern.endswith("*"):
318
- match_pattern = ignored_pattern[:-1]
319
- # Match wildcard against the provider's model name
320
- if provider_model_name.startswith(match_pattern):
321
- return True
322
- else:
323
- # Exact match against the full proxy ID OR the provider's model name
324
- if (
325
- model_id == ignored_pattern
326
- or provider_model_name == ignored_pattern
327
- ):
328
- return True
329
  return False
330
 
331
  def _is_model_whitelisted(self, provider: str, model_id: str) -> bool:
332
  """
333
  Checks if a model is explicitly whitelisted.
334
- Supports exact and partial matching for both full model IDs and model names.
 
 
 
 
 
 
 
335
  """
336
  model_provider = model_id.split("/")[0]
337
  if model_provider not in self.whitelist_models:
338
  return False
339
 
340
  whitelist = self.whitelist_models[model_provider]
 
 
 
 
 
 
 
341
  for whitelisted_pattern in whitelist:
342
- if whitelisted_pattern == "*":
 
 
 
343
  return True
344
-
345
- try:
346
- # This is the model name as the provider sees it (e.g., "gpt-4" or "google/gemma-7b")
347
- provider_model_name = model_id.split("/", 1)[1]
348
- except IndexError:
349
- provider_model_name = model_id
350
-
351
- if whitelisted_pattern.endswith("*"):
352
- match_pattern = whitelisted_pattern[:-1]
353
- # Match wildcard against the provider's model name
354
- if provider_model_name.startswith(match_pattern):
355
- return True
356
- else:
357
- # Exact match against the full proxy ID OR the provider's model name
358
- if (
359
- model_id == whitelisted_pattern
360
- or provider_model_name == whitelisted_pattern
361
- ):
362
- return True
363
  return False
364
 
365
  def _sanitize_litellm_log(self, log_data: dict) -> dict:
 
1
  import asyncio
2
+ import fnmatch
3
  import json
4
  import re
5
  import codecs
 
298
  def _is_model_ignored(self, provider: str, model_id: str) -> bool:
299
  """
300
  Checks if a model should be ignored based on the ignore list.
301
+ Supports full glob/fnmatch patterns for both full model IDs and model names.
302
+
303
+ Pattern examples:
304
+ - "gpt-4" - exact match
305
+ - "gpt-4*" - prefix wildcard (matches gpt-4, gpt-4-turbo, etc.)
306
+ - "*-preview" - suffix wildcard (matches gpt-4-preview, o1-preview, etc.)
307
+ - "*-preview*" - contains wildcard (matches anything with -preview)
308
+ - "*" - match all
309
  """
310
  model_provider = model_id.split("/")[0]
311
  if model_provider not in self.ignore_models:
 
322
  provider_model_name = model_id
323
 
324
  for ignored_pattern in ignore_list:
325
+ # Use fnmatch for full glob pattern support
326
+ if fnmatch.fnmatch(provider_model_name, ignored_pattern) or fnmatch.fnmatch(
327
+ model_id, ignored_pattern
328
+ ):
329
+ return True
 
 
 
 
 
 
 
330
  return False
331
 
332
  def _is_model_whitelisted(self, provider: str, model_id: str) -> bool:
333
  """
334
  Checks if a model is explicitly whitelisted.
335
+ Supports full glob/fnmatch patterns for both full model IDs and model names.
336
+
337
+ Pattern examples:
338
+ - "gpt-4" - exact match
339
+ - "gpt-4*" - prefix wildcard (matches gpt-4, gpt-4-turbo, etc.)
340
+ - "*-preview" - suffix wildcard (matches gpt-4-preview, o1-preview, etc.)
341
+ - "*-preview*" - contains wildcard (matches anything with -preview)
342
+ - "*" - match all
343
  """
344
  model_provider = model_id.split("/")[0]
345
  if model_provider not in self.whitelist_models:
346
  return False
347
 
348
  whitelist = self.whitelist_models[model_provider]
349
+
350
+ try:
351
+ # This is the model name as the provider sees it (e.g., "gpt-4" or "google/gemma-7b")
352
+ provider_model_name = model_id.split("/", 1)[1]
353
+ except IndexError:
354
+ provider_model_name = model_id
355
+
356
  for whitelisted_pattern in whitelist:
357
+ # Use fnmatch for full glob pattern support
358
+ if fnmatch.fnmatch(
359
+ provider_model_name, whitelisted_pattern
360
+ ) or fnmatch.fnmatch(model_id, whitelisted_pattern):
361
  return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  return False
363
 
364
  def _sanitize_litellm_log(self, log_data: dict) -> dict:
src/rotator_library/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "rotator_library"
7
- version = "1.0"
8
  authors = [
9
  { name="Mirrowel", email="nuh@uh.com" },
10
  ]
@@ -16,11 +16,7 @@ classifiers = [
16
  "License :: OSI Approved :: MIT License",
17
  "Operating System :: OS Independent",
18
  ]
19
- dependencies = [
20
- "litellm",
21
- "filelock",
22
- "httpx"
23
- ]
24
 
25
  [project.urls]
26
  "Homepage" = "https://github.com/Mirrowel/LLM-API-Key-Proxy"
 
4
 
5
  [project]
6
  name = "rotator_library"
7
+ version = "1.05"
8
  authors = [
9
  { name="Mirrowel", email="nuh@uh.com" },
10
  ]
 
16
  "License :: OSI Approved :: MIT License",
17
  "Operating System :: OS Independent",
18
  ]
19
+ dependencies = []
 
 
 
 
20
 
21
  [project.urls]
22
  "Homepage" = "https://github.com/Mirrowel/LLM-API-Key-Proxy"