Spaces:
Paused
feat(settings): ✨ add provider-specific settings management UI
Browse filesIntroduces a comprehensive provider-specific settings management system for Antigravity and Gemini CLI providers with detection, display, and interactive configuration capabilities.
- Add `PROVIDER_SETTINGS_MAP` with detailed definitions for Antigravity (12 settings) and Gemini CLI (8 settings) including signature caching, tool fixes, and provider-specific parameters
- Implement `ProviderSettingsManager` class for managing provider settings with type-aware value parsing and modification tracking
- Add `detect_provider_settings()` method to `SettingsDetector` to identify modified provider settings from environment variables
- Integrate provider settings detection into launcher TUI summary display and detailed advanced settings view
- Add new menu option (4) in settings tool for provider-specific configuration management
- Implement interactive TUI for browsing, editing, and resetting individual or all provider settings with visual indication of modified values
- Display provider settings status in launcher with count of modified settings per provider
- Support bool, int, and string setting types with appropriate input handling and validation
- src/proxy_app/launcher_tui.py +61 -3
- src/proxy_app/settings_tool.py +375 -4
|
@@ -100,7 +100,8 @@ class SettingsDetector:
|
|
| 100 |
"custom_bases": SettingsDetector.detect_custom_api_bases(),
|
| 101 |
"model_definitions": SettingsDetector.detect_model_definitions(),
|
| 102 |
"concurrency_limits": SettingsDetector.detect_concurrency_limits(),
|
| 103 |
-
"model_filters": SettingsDetector.detect_model_filters()
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
@staticmethod
|
|
@@ -198,6 +199,45 @@ class SettingsDetector:
|
|
| 198 |
else:
|
| 199 |
filters[provider]["has_whitelist"] = True
|
| 200 |
return filters
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
|
| 203 |
class LauncherTUI:
|
|
@@ -300,7 +340,8 @@ class LauncherTUI:
|
|
| 300 |
self.console.print("━" * 70)
|
| 301 |
provider_count = len(credentials)
|
| 302 |
custom_count = len(custom_bases)
|
| 303 |
-
|
|
|
|
| 304 |
|
| 305 |
self.console.print(f" Providers: {provider_count} configured")
|
| 306 |
self.console.print(f" Custom Providers: {custom_count} configured")
|
|
@@ -422,6 +463,7 @@ class LauncherTUI:
|
|
| 422 |
model_defs = settings["model_definitions"]
|
| 423 |
concurrency = settings["concurrency_limits"]
|
| 424 |
filters = settings["model_filters"]
|
|
|
|
| 425 |
|
| 426 |
self.console.print(Panel.fit(
|
| 427 |
"[bold cyan]📊 Provider & Advanced Settings[/bold cyan]",
|
|
@@ -472,7 +514,7 @@ class LauncherTUI:
|
|
| 472 |
self.console.print("━" * 70)
|
| 473 |
for provider, limit in concurrency.items():
|
| 474 |
self.console.print(f" • {provider:15} {limit} requests/key")
|
| 475 |
-
self.console.print(
|
| 476 |
|
| 477 |
# Model Filters (basic info only)
|
| 478 |
if filters:
|
|
@@ -488,6 +530,22 @@ class LauncherTUI:
|
|
| 488 |
status = " + ".join(status_parts) if status_parts else "None"
|
| 489 |
self.console.print(f" • {provider:15} ✅ {status}")
|
| 490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
# Actions
|
| 492 |
self.console.print()
|
| 493 |
self.console.print("━" * 70)
|
|
|
|
| 100 |
"custom_bases": SettingsDetector.detect_custom_api_bases(),
|
| 101 |
"model_definitions": SettingsDetector.detect_model_definitions(),
|
| 102 |
"concurrency_limits": SettingsDetector.detect_concurrency_limits(),
|
| 103 |
+
"model_filters": SettingsDetector.detect_model_filters(),
|
| 104 |
+
"provider_settings": SettingsDetector.detect_provider_settings()
|
| 105 |
}
|
| 106 |
|
| 107 |
@staticmethod
|
|
|
|
| 199 |
else:
|
| 200 |
filters[provider]["has_whitelist"] = True
|
| 201 |
return filters
|
| 202 |
+
|
| 203 |
+
@staticmethod
|
| 204 |
+
def detect_provider_settings() -> dict:
|
| 205 |
+
"""Detect provider-specific settings (Antigravity, Gemini CLI)"""
|
| 206 |
+
try:
|
| 207 |
+
from proxy_app.settings_tool import PROVIDER_SETTINGS_MAP
|
| 208 |
+
except ImportError:
|
| 209 |
+
# Fallback for direct execution or testing
|
| 210 |
+
from .settings_tool import PROVIDER_SETTINGS_MAP
|
| 211 |
+
|
| 212 |
+
provider_settings = {}
|
| 213 |
+
env_vars = SettingsDetector._load_local_env()
|
| 214 |
+
|
| 215 |
+
for provider, definitions in PROVIDER_SETTINGS_MAP.items():
|
| 216 |
+
modified_count = 0
|
| 217 |
+
for key, definition in definitions.items():
|
| 218 |
+
env_value = env_vars.get(key)
|
| 219 |
+
if env_value is not None:
|
| 220 |
+
# Check if value differs from default
|
| 221 |
+
default = definition.get("default")
|
| 222 |
+
setting_type = definition.get("type", "str")
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
if setting_type == "bool":
|
| 226 |
+
current = env_value.lower() in ("true", "1", "yes")
|
| 227 |
+
elif setting_type == "int":
|
| 228 |
+
current = int(env_value)
|
| 229 |
+
else:
|
| 230 |
+
current = env_value
|
| 231 |
+
|
| 232 |
+
if current != default:
|
| 233 |
+
modified_count += 1
|
| 234 |
+
except (ValueError, AttributeError):
|
| 235 |
+
pass
|
| 236 |
+
|
| 237 |
+
if modified_count > 0:
|
| 238 |
+
provider_settings[provider] = modified_count
|
| 239 |
+
|
| 240 |
+
return provider_settings
|
| 241 |
|
| 242 |
|
| 243 |
class LauncherTUI:
|
|
|
|
| 340 |
self.console.print("━" * 70)
|
| 341 |
provider_count = len(credentials)
|
| 342 |
custom_count = len(custom_bases)
|
| 343 |
+
provider_settings = settings.get("provider_settings", {})
|
| 344 |
+
has_advanced = bool(settings["model_definitions"] or settings["concurrency_limits"] or settings["model_filters"] or provider_settings)
|
| 345 |
|
| 346 |
self.console.print(f" Providers: {provider_count} configured")
|
| 347 |
self.console.print(f" Custom Providers: {custom_count} configured")
|
|
|
|
| 463 |
model_defs = settings["model_definitions"]
|
| 464 |
concurrency = settings["concurrency_limits"]
|
| 465 |
filters = settings["model_filters"]
|
| 466 |
+
provider_settings = settings.get("provider_settings", {})
|
| 467 |
|
| 468 |
self.console.print(Panel.fit(
|
| 469 |
"[bold cyan]📊 Provider & Advanced Settings[/bold cyan]",
|
|
|
|
| 514 |
self.console.print("━" * 70)
|
| 515 |
for provider, limit in concurrency.items():
|
| 516 |
self.console.print(f" • {provider:15} {limit} requests/key")
|
| 517 |
+
self.console.print(" • Default: 1 request/key (all others)")
|
| 518 |
|
| 519 |
# Model Filters (basic info only)
|
| 520 |
if filters:
|
|
|
|
| 530 |
status = " + ".join(status_parts) if status_parts else "None"
|
| 531 |
self.console.print(f" • {provider:15} ✅ {status}")
|
| 532 |
|
| 533 |
+
# Provider-Specific Settings
|
| 534 |
+
self.console.print()
|
| 535 |
+
self.console.print("[bold]🔬 Provider-Specific Settings[/bold]")
|
| 536 |
+
self.console.print("━" * 70)
|
| 537 |
+
try:
|
| 538 |
+
from proxy_app.settings_tool import PROVIDER_SETTINGS_MAP
|
| 539 |
+
except ImportError:
|
| 540 |
+
from .settings_tool import PROVIDER_SETTINGS_MAP
|
| 541 |
+
for provider in PROVIDER_SETTINGS_MAP.keys():
|
| 542 |
+
display_name = provider.replace("_", " ").title()
|
| 543 |
+
modified = provider_settings.get(provider, 0)
|
| 544 |
+
if modified > 0:
|
| 545 |
+
self.console.print(f" • {display_name:20} [yellow]{modified} setting{'s' if modified > 1 else ''} modified[/yellow]")
|
| 546 |
+
else:
|
| 547 |
+
self.console.print(f" • {display_name:20} [dim]using defaults[/dim]")
|
| 548 |
+
|
| 549 |
# Actions
|
| 550 |
self.console.print()
|
| 551 |
self.console.print("━" * 70)
|
|
@@ -166,6 +166,184 @@ class ConcurrencyManager:
|
|
| 166 |
self.settings.remove(key)
|
| 167 |
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
class SettingsTool:
|
| 170 |
"""Main settings tool TUI"""
|
| 171 |
|
|
@@ -175,6 +353,7 @@ class SettingsTool:
|
|
| 175 |
self.provider_mgr = CustomProviderManager(self.settings)
|
| 176 |
self.model_mgr = ModelDefinitionManager(self.settings)
|
| 177 |
self.concurrency_mgr = ConcurrencyManager(self.settings)
|
|
|
|
| 178 |
self.running = True
|
| 179 |
|
| 180 |
def get_available_providers(self) -> List[str]:
|
|
@@ -223,8 +402,9 @@ class SettingsTool:
|
|
| 223 |
self.console.print(" 1. 🌐 Custom Provider API Bases")
|
| 224 |
self.console.print(" 2. 📦 Provider Model Definitions")
|
| 225 |
self.console.print(" 3. ⚡ Concurrency Limits")
|
| 226 |
-
self.console.print(" 4.
|
| 227 |
-
self.console.print(" 5.
|
|
|
|
| 228 |
|
| 229 |
self.console.print()
|
| 230 |
self.console.print("━" * 70)
|
|
@@ -238,7 +418,7 @@ class SettingsTool:
|
|
| 238 |
self.console.print("[dim]⚠️ Model filters not supported - edit .env for IGNORE_MODELS_* / WHITELIST_MODELS_*[/dim]")
|
| 239 |
self.console.print()
|
| 240 |
|
| 241 |
-
choice = Prompt.ask("Select option", choices=["1", "2", "3", "4", "5"], show_choices=False)
|
| 242 |
|
| 243 |
if choice == "1":
|
| 244 |
self.manage_custom_providers()
|
|
@@ -247,8 +427,10 @@ class SettingsTool:
|
|
| 247 |
elif choice == "3":
|
| 248 |
self.manage_concurrency_limits()
|
| 249 |
elif choice == "4":
|
| 250 |
-
self.
|
| 251 |
elif choice == "5":
|
|
|
|
|
|
|
| 252 |
self.exit_without_saving()
|
| 253 |
|
| 254 |
def manage_custom_providers(self):
|
|
@@ -631,6 +813,195 @@ class SettingsTool:
|
|
| 631 |
|
| 632 |
input("Press Enter to return...")
|
| 633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
def manage_concurrency_limits(self):
|
| 635 |
"""Manage concurrency limits"""
|
| 636 |
while True:
|
|
|
|
| 166 |
self.settings.remove(key)
|
| 167 |
|
| 168 |
|
| 169 |
+
# =============================================================================
|
| 170 |
+
# PROVIDER-SPECIFIC SETTINGS DEFINITIONS
|
| 171 |
+
# =============================================================================
|
| 172 |
+
|
| 173 |
+
# Antigravity provider environment variables
|
| 174 |
+
ANTIGRAVITY_SETTINGS = {
|
| 175 |
+
"ANTIGRAVITY_SIGNATURE_CACHE_TTL": {
|
| 176 |
+
"type": "int",
|
| 177 |
+
"default": 3600,
|
| 178 |
+
"description": "Memory cache TTL for Gemini 3 thought signatures (seconds)",
|
| 179 |
+
},
|
| 180 |
+
"ANTIGRAVITY_SIGNATURE_DISK_TTL": {
|
| 181 |
+
"type": "int",
|
| 182 |
+
"default": 86400,
|
| 183 |
+
"description": "Disk cache TTL for Gemini 3 thought signatures (seconds)",
|
| 184 |
+
},
|
| 185 |
+
"ANTIGRAVITY_PRESERVE_THOUGHT_SIGNATURES": {
|
| 186 |
+
"type": "bool",
|
| 187 |
+
"default": True,
|
| 188 |
+
"description": "Preserve thought signatures in client responses",
|
| 189 |
+
},
|
| 190 |
+
"ANTIGRAVITY_ENABLE_SIGNATURE_CACHE": {
|
| 191 |
+
"type": "bool",
|
| 192 |
+
"default": True,
|
| 193 |
+
"description": "Enable signature caching for multi-turn conversations",
|
| 194 |
+
},
|
| 195 |
+
"ANTIGRAVITY_ENABLE_DYNAMIC_MODELS": {
|
| 196 |
+
"type": "bool",
|
| 197 |
+
"default": False,
|
| 198 |
+
"description": "Enable dynamic model discovery from API",
|
| 199 |
+
},
|
| 200 |
+
"ANTIGRAVITY_GEMINI3_TOOL_FIX": {
|
| 201 |
+
"type": "bool",
|
| 202 |
+
"default": True,
|
| 203 |
+
"description": "Enable Gemini 3 tool hallucination prevention",
|
| 204 |
+
},
|
| 205 |
+
"ANTIGRAVITY_CLAUDE_TOOL_FIX": {
|
| 206 |
+
"type": "bool",
|
| 207 |
+
"default": True,
|
| 208 |
+
"description": "Enable Claude tool hallucination prevention",
|
| 209 |
+
},
|
| 210 |
+
"ANTIGRAVITY_CLAUDE_THINKING_SANITIZATION": {
|
| 211 |
+
"type": "bool",
|
| 212 |
+
"default": True,
|
| 213 |
+
"description": "Sanitize thinking blocks for Claude multi-turn conversations",
|
| 214 |
+
},
|
| 215 |
+
"ANTIGRAVITY_GEMINI3_TOOL_PREFIX": {
|
| 216 |
+
"type": "str",
|
| 217 |
+
"default": "gemini3_",
|
| 218 |
+
"description": "Prefix added to tool names for Gemini 3 disambiguation",
|
| 219 |
+
},
|
| 220 |
+
"ANTIGRAVITY_GEMINI3_DESCRIPTION_PROMPT": {
|
| 221 |
+
"type": "str",
|
| 222 |
+
"default": "\n\nSTRICT PARAMETERS: {params}.",
|
| 223 |
+
"description": "Template for strict parameter hints in tool descriptions",
|
| 224 |
+
},
|
| 225 |
+
"ANTIGRAVITY_CLAUDE_DESCRIPTION_PROMPT": {
|
| 226 |
+
"type": "str",
|
| 227 |
+
"default": "\n\nSTRICT PARAMETERS: {params}.",
|
| 228 |
+
"description": "Template for Claude strict parameter hints in tool descriptions",
|
| 229 |
+
},
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# Gemini CLI provider environment variables
|
| 233 |
+
GEMINI_CLI_SETTINGS = {
|
| 234 |
+
"GEMINI_CLI_SIGNATURE_CACHE_TTL": {
|
| 235 |
+
"type": "int",
|
| 236 |
+
"default": 3600,
|
| 237 |
+
"description": "Memory cache TTL for thought signatures (seconds)",
|
| 238 |
+
},
|
| 239 |
+
"GEMINI_CLI_SIGNATURE_DISK_TTL": {
|
| 240 |
+
"type": "int",
|
| 241 |
+
"default": 86400,
|
| 242 |
+
"description": "Disk cache TTL for thought signatures (seconds)",
|
| 243 |
+
},
|
| 244 |
+
"GEMINI_CLI_PRESERVE_THOUGHT_SIGNATURES": {
|
| 245 |
+
"type": "bool",
|
| 246 |
+
"default": True,
|
| 247 |
+
"description": "Preserve thought signatures in client responses",
|
| 248 |
+
},
|
| 249 |
+
"GEMINI_CLI_ENABLE_SIGNATURE_CACHE": {
|
| 250 |
+
"type": "bool",
|
| 251 |
+
"default": True,
|
| 252 |
+
"description": "Enable signature caching for multi-turn conversations",
|
| 253 |
+
},
|
| 254 |
+
"GEMINI_CLI_GEMINI3_TOOL_FIX": {
|
| 255 |
+
"type": "bool",
|
| 256 |
+
"default": True,
|
| 257 |
+
"description": "Enable Gemini 3 tool hallucination prevention",
|
| 258 |
+
},
|
| 259 |
+
"GEMINI_CLI_GEMINI3_TOOL_PREFIX": {
|
| 260 |
+
"type": "str",
|
| 261 |
+
"default": "gemini3_",
|
| 262 |
+
"description": "Prefix added to tool names for Gemini 3 disambiguation",
|
| 263 |
+
},
|
| 264 |
+
"GEMINI_CLI_GEMINI3_DESCRIPTION_PROMPT": {
|
| 265 |
+
"type": "str",
|
| 266 |
+
"default": "\n\nSTRICT PARAMETERS: {params}.",
|
| 267 |
+
"description": "Template for strict parameter hints in tool descriptions",
|
| 268 |
+
},
|
| 269 |
+
"GEMINI_CLI_PROJECT_ID": {
|
| 270 |
+
"type": "str",
|
| 271 |
+
"default": "",
|
| 272 |
+
"description": "GCP Project ID for paid tier users (required for paid tiers)",
|
| 273 |
+
},
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
# Map provider names to their settings definitions
|
| 277 |
+
PROVIDER_SETTINGS_MAP = {
|
| 278 |
+
"antigravity": ANTIGRAVITY_SETTINGS,
|
| 279 |
+
"gemini_cli": GEMINI_CLI_SETTINGS,
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class ProviderSettingsManager:
|
| 284 |
+
"""Manages provider-specific configuration settings"""
|
| 285 |
+
|
| 286 |
+
def __init__(self, settings: AdvancedSettings):
|
| 287 |
+
self.settings = settings
|
| 288 |
+
|
| 289 |
+
def get_available_providers(self) -> List[str]:
|
| 290 |
+
"""Get list of providers with specific settings available"""
|
| 291 |
+
return list(PROVIDER_SETTINGS_MAP.keys())
|
| 292 |
+
|
| 293 |
+
def get_provider_settings_definitions(self, provider: str) -> Dict[str, Dict[str, Any]]:
|
| 294 |
+
"""Get settings definitions for a provider"""
|
| 295 |
+
return PROVIDER_SETTINGS_MAP.get(provider, {})
|
| 296 |
+
|
| 297 |
+
def get_current_value(self, key: str, definition: Dict[str, Any]) -> Any:
|
| 298 |
+
"""Get current value of a setting from environment"""
|
| 299 |
+
env_value = os.getenv(key)
|
| 300 |
+
if env_value is None:
|
| 301 |
+
return definition.get("default")
|
| 302 |
+
|
| 303 |
+
setting_type = definition.get("type", "str")
|
| 304 |
+
try:
|
| 305 |
+
if setting_type == "bool":
|
| 306 |
+
return env_value.lower() in ("true", "1", "yes")
|
| 307 |
+
elif setting_type == "int":
|
| 308 |
+
return int(env_value)
|
| 309 |
+
else:
|
| 310 |
+
return env_value
|
| 311 |
+
except (ValueError, AttributeError):
|
| 312 |
+
return definition.get("default")
|
| 313 |
+
|
| 314 |
+
def get_all_current_values(self, provider: str) -> Dict[str, Any]:
|
| 315 |
+
"""Get all current values for a provider"""
|
| 316 |
+
definitions = self.get_provider_settings_definitions(provider)
|
| 317 |
+
values = {}
|
| 318 |
+
for key, definition in definitions.items():
|
| 319 |
+
values[key] = self.get_current_value(key, definition)
|
| 320 |
+
return values
|
| 321 |
+
|
| 322 |
+
def set_value(self, key: str, value: Any, definition: Dict[str, Any]):
|
| 323 |
+
"""Set a setting value, converting to string for .env storage"""
|
| 324 |
+
setting_type = definition.get("type", "str")
|
| 325 |
+
if setting_type == "bool":
|
| 326 |
+
str_value = "true" if value else "false"
|
| 327 |
+
else:
|
| 328 |
+
str_value = str(value)
|
| 329 |
+
self.settings.set(key, str_value)
|
| 330 |
+
|
| 331 |
+
def reset_to_default(self, key: str):
|
| 332 |
+
"""Remove a setting to reset it to default"""
|
| 333 |
+
self.settings.remove(key)
|
| 334 |
+
|
| 335 |
+
def get_modified_settings(self, provider: str) -> Dict[str, Any]:
|
| 336 |
+
"""Get settings that differ from defaults"""
|
| 337 |
+
definitions = self.get_provider_settings_definitions(provider)
|
| 338 |
+
modified = {}
|
| 339 |
+
for key, definition in definitions.items():
|
| 340 |
+
current = self.get_current_value(key, definition)
|
| 341 |
+
default = definition.get("default")
|
| 342 |
+
if current != default:
|
| 343 |
+
modified[key] = current
|
| 344 |
+
return modified
|
| 345 |
+
|
| 346 |
+
|
| 347 |
class SettingsTool:
|
| 348 |
"""Main settings tool TUI"""
|
| 349 |
|
|
|
|
| 353 |
self.provider_mgr = CustomProviderManager(self.settings)
|
| 354 |
self.model_mgr = ModelDefinitionManager(self.settings)
|
| 355 |
self.concurrency_mgr = ConcurrencyManager(self.settings)
|
| 356 |
+
self.provider_settings_mgr = ProviderSettingsManager(self.settings)
|
| 357 |
self.running = True
|
| 358 |
|
| 359 |
def get_available_providers(self) -> List[str]:
|
|
|
|
| 402 |
self.console.print(" 1. 🌐 Custom Provider API Bases")
|
| 403 |
self.console.print(" 2. 📦 Provider Model Definitions")
|
| 404 |
self.console.print(" 3. ⚡ Concurrency Limits")
|
| 405 |
+
self.console.print(" 4. 🔬 Provider-Specific Settings")
|
| 406 |
+
self.console.print(" 5. 💾 Save & Exit")
|
| 407 |
+
self.console.print(" 6. 🚫 Exit Without Saving")
|
| 408 |
|
| 409 |
self.console.print()
|
| 410 |
self.console.print("━" * 70)
|
|
|
|
| 418 |
self.console.print("[dim]⚠️ Model filters not supported - edit .env for IGNORE_MODELS_* / WHITELIST_MODELS_*[/dim]")
|
| 419 |
self.console.print()
|
| 420 |
|
| 421 |
+
choice = Prompt.ask("Select option", choices=["1", "2", "3", "4", "5", "6"], show_choices=False)
|
| 422 |
|
| 423 |
if choice == "1":
|
| 424 |
self.manage_custom_providers()
|
|
|
|
| 427 |
elif choice == "3":
|
| 428 |
self.manage_concurrency_limits()
|
| 429 |
elif choice == "4":
|
| 430 |
+
self.manage_provider_settings()
|
| 431 |
elif choice == "5":
|
| 432 |
+
self.save_and_exit()
|
| 433 |
+
elif choice == "6":
|
| 434 |
self.exit_without_saving()
|
| 435 |
|
| 436 |
def manage_custom_providers(self):
|
|
|
|
| 813 |
|
| 814 |
input("Press Enter to return...")
|
| 815 |
|
| 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 |
+
|
| 823 |
+
self.console.print(Panel.fit(
|
| 824 |
+
"[bold cyan]🔬 Provider-Specific Settings[/bold cyan]",
|
| 825 |
+
border_style="cyan"
|
| 826 |
+
))
|
| 827 |
+
|
| 828 |
+
self.console.print()
|
| 829 |
+
self.console.print("[bold]📋 Available Providers with Custom Settings[/bold]")
|
| 830 |
+
self.console.print("━" * 70)
|
| 831 |
+
|
| 832 |
+
for provider in available_providers:
|
| 833 |
+
modified = self.provider_settings_mgr.get_modified_settings(provider)
|
| 834 |
+
status = f"[yellow]{len(modified)} modified[/yellow]" if modified else "[dim]defaults[/dim]"
|
| 835 |
+
display_name = provider.replace("_", " ").title()
|
| 836 |
+
self.console.print(f" • {display_name:20} {status}")
|
| 837 |
+
|
| 838 |
+
self.console.print()
|
| 839 |
+
self.console.print("━" * 70)
|
| 840 |
+
self.console.print()
|
| 841 |
+
self.console.print("[bold]⚙️ Select Provider to Configure[/bold]")
|
| 842 |
+
self.console.print()
|
| 843 |
+
|
| 844 |
+
for idx, provider in enumerate(available_providers, 1):
|
| 845 |
+
display_name = provider.replace("_", " ").title()
|
| 846 |
+
self.console.print(f" {idx}. {display_name}")
|
| 847 |
+
self.console.print(f" {len(available_providers) + 1}. ↩️ Back to Settings Menu")
|
| 848 |
+
|
| 849 |
+
self.console.print()
|
| 850 |
+
self.console.print("━" * 70)
|
| 851 |
+
self.console.print()
|
| 852 |
+
|
| 853 |
+
choices = [str(i) for i in range(1, len(available_providers) + 2)]
|
| 854 |
+
choice = Prompt.ask("Select option", choices=choices, show_choices=False)
|
| 855 |
+
choice_idx = int(choice)
|
| 856 |
+
|
| 857 |
+
if choice_idx == len(available_providers) + 1:
|
| 858 |
+
break
|
| 859 |
+
|
| 860 |
+
provider = available_providers[choice_idx - 1]
|
| 861 |
+
self._manage_single_provider_settings(provider)
|
| 862 |
+
|
| 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)
|
| 870 |
+
current_values = self.provider_settings_mgr.get_all_current_values(provider)
|
| 871 |
+
|
| 872 |
+
self.console.print(Panel.fit(
|
| 873 |
+
f"[bold cyan]🔬 {display_name} Settings[/bold cyan]",
|
| 874 |
+
border_style="cyan"
|
| 875 |
+
))
|
| 876 |
+
|
| 877 |
+
self.console.print()
|
| 878 |
+
self.console.print("[bold]📋 Current Settings[/bold]")
|
| 879 |
+
self.console.print("━" * 70)
|
| 880 |
+
|
| 881 |
+
# Display all settings with current values
|
| 882 |
+
settings_list = list(definitions.keys())
|
| 883 |
+
for idx, key in enumerate(settings_list, 1):
|
| 884 |
+
definition = definitions[key]
|
| 885 |
+
current = current_values.get(key)
|
| 886 |
+
default = definition.get("default")
|
| 887 |
+
setting_type = definition.get("type", "str")
|
| 888 |
+
description = definition.get("description", "")
|
| 889 |
+
|
| 890 |
+
# Format value display
|
| 891 |
+
if setting_type == "bool":
|
| 892 |
+
value_display = "[green]✓ Enabled[/green]" if current else "[red]✗ Disabled[/red]"
|
| 893 |
+
elif setting_type == "int":
|
| 894 |
+
value_display = f"[cyan]{current}[/cyan]"
|
| 895 |
+
else:
|
| 896 |
+
value_display = f"[cyan]{current or '(not set)'}[/cyan]" if current else "[dim](not set)[/dim]"
|
| 897 |
+
|
| 898 |
+
# Check if modified from default
|
| 899 |
+
modified = current != default
|
| 900 |
+
mod_marker = "[yellow]*[/yellow]" if modified else " "
|
| 901 |
+
|
| 902 |
+
# Short key name for display (strip provider prefix)
|
| 903 |
+
short_key = key.replace(f"{provider.upper()}_", "")
|
| 904 |
+
|
| 905 |
+
self.console.print(f" {mod_marker}{idx:2}. {short_key:35} {value_display}")
|
| 906 |
+
self.console.print(f" [dim]{description}[/dim]")
|
| 907 |
+
|
| 908 |
+
self.console.print()
|
| 909 |
+
self.console.print("━" * 70)
|
| 910 |
+
self.console.print("[dim]* = modified from default[/dim]")
|
| 911 |
+
self.console.print()
|
| 912 |
+
self.console.print("[bold]⚙️ Actions[/bold]")
|
| 913 |
+
self.console.print()
|
| 914 |
+
self.console.print(" E. ✏️ Edit a Setting")
|
| 915 |
+
self.console.print(" R. 🔄 Reset Setting to Default")
|
| 916 |
+
self.console.print(" A. 🔄 Reset All to Defaults")
|
| 917 |
+
self.console.print(" B. ↩️ Back to Provider Selection")
|
| 918 |
+
|
| 919 |
+
self.console.print()
|
| 920 |
+
self.console.print("━" * 70)
|
| 921 |
+
self.console.print()
|
| 922 |
+
|
| 923 |
+
choice = Prompt.ask("Select action", choices=["e", "r", "a", "b", "E", "R", "A", "B"], show_choices=False).lower()
|
| 924 |
+
|
| 925 |
+
if choice == "b":
|
| 926 |
+
break
|
| 927 |
+
elif choice == "e":
|
| 928 |
+
self._edit_provider_setting(provider, settings_list, definitions)
|
| 929 |
+
elif choice == "r":
|
| 930 |
+
self._reset_provider_setting(provider, settings_list, definitions)
|
| 931 |
+
elif choice == "a":
|
| 932 |
+
self._reset_all_provider_settings(provider, settings_list)
|
| 933 |
+
|
| 934 |
+
def _edit_provider_setting(self, provider: str, settings_list: List[str], definitions: Dict[str, Dict[str, Any]]):
|
| 935 |
+
"""Edit a single provider setting"""
|
| 936 |
+
self.console.print("\n[bold]Select setting number to edit:[/bold]")
|
| 937 |
+
|
| 938 |
+
choices = [str(i) for i in range(1, len(settings_list) + 1)]
|
| 939 |
+
choice = IntPrompt.ask("Setting number", choices=choices)
|
| 940 |
+
key = settings_list[choice - 1]
|
| 941 |
+
definition = definitions[key]
|
| 942 |
+
|
| 943 |
+
current = self.provider_settings_mgr.get_current_value(key, definition)
|
| 944 |
+
default = definition.get("default")
|
| 945 |
+
setting_type = definition.get("type", "str")
|
| 946 |
+
short_key = key.replace(f"{provider.upper()}_", "")
|
| 947 |
+
|
| 948 |
+
self.console.print(f"\n[bold]Editing: {short_key}[/bold]")
|
| 949 |
+
self.console.print(f"Current value: [cyan]{current}[/cyan]")
|
| 950 |
+
self.console.print(f"Default value: [dim]{default}[/dim]")
|
| 951 |
+
self.console.print(f"Type: {setting_type}")
|
| 952 |
+
|
| 953 |
+
if setting_type == "bool":
|
| 954 |
+
new_value = Confirm.ask("\nEnable this setting?", default=current)
|
| 955 |
+
self.provider_settings_mgr.set_value(key, new_value, definition)
|
| 956 |
+
status = "enabled" if new_value else "disabled"
|
| 957 |
+
self.console.print(f"\n[green]✅ {short_key} {status}![/green]")
|
| 958 |
+
elif setting_type == "int":
|
| 959 |
+
new_value = IntPrompt.ask("\nNew value", default=current)
|
| 960 |
+
self.provider_settings_mgr.set_value(key, new_value, definition)
|
| 961 |
+
self.console.print(f"\n[green]✅ {short_key} set to {new_value}![/green]")
|
| 962 |
+
else:
|
| 963 |
+
new_value = Prompt.ask("\nNew value", default=str(current) if current else "").strip()
|
| 964 |
+
if new_value:
|
| 965 |
+
self.provider_settings_mgr.set_value(key, new_value, definition)
|
| 966 |
+
self.console.print(f"\n[green]✅ {short_key} updated![/green]")
|
| 967 |
+
else:
|
| 968 |
+
self.console.print("\n[yellow]No changes made[/yellow]")
|
| 969 |
+
|
| 970 |
+
input("\nPress Enter to continue...")
|
| 971 |
+
|
| 972 |
+
def _reset_provider_setting(self, provider: str, settings_list: List[str], definitions: Dict[str, Dict[str, Any]]):
|
| 973 |
+
"""Reset a single provider setting to default"""
|
| 974 |
+
self.console.print("\n[bold]Select setting number to reset:[/bold]")
|
| 975 |
+
|
| 976 |
+
choices = [str(i) for i in range(1, len(settings_list) + 1)]
|
| 977 |
+
choice = IntPrompt.ask("Setting number", choices=choices)
|
| 978 |
+
key = settings_list[choice - 1]
|
| 979 |
+
definition = definitions[key]
|
| 980 |
+
|
| 981 |
+
default = definition.get("default")
|
| 982 |
+
short_key = key.replace(f"{provider.upper()}_", "")
|
| 983 |
+
|
| 984 |
+
if Confirm.ask(f"\nReset {short_key} to default ({default})?"):
|
| 985 |
+
self.provider_settings_mgr.reset_to_default(key)
|
| 986 |
+
self.console.print(f"\n[green]✅ {short_key} reset to default![/green]")
|
| 987 |
+
else:
|
| 988 |
+
self.console.print("\n[yellow]No changes made[/yellow]")
|
| 989 |
+
|
| 990 |
+
input("\nPress Enter to continue...")
|
| 991 |
+
|
| 992 |
+
def _reset_all_provider_settings(self, provider: str, settings_list: List[str]):
|
| 993 |
+
"""Reset all provider settings to defaults"""
|
| 994 |
+
display_name = provider.replace("_", " ").title()
|
| 995 |
+
|
| 996 |
+
if Confirm.ask(f"\n[bold red]Reset ALL {display_name} settings to defaults?[/bold red]"):
|
| 997 |
+
for key in settings_list:
|
| 998 |
+
self.provider_settings_mgr.reset_to_default(key)
|
| 999 |
+
self.console.print(f"\n[green]✅ All {display_name} settings reset to defaults![/green]")
|
| 1000 |
+
else:
|
| 1001 |
+
self.console.print("\n[yellow]No changes made[/yellow]")
|
| 1002 |
+
|
| 1003 |
+
input("\nPress Enter to continue...")
|
| 1004 |
+
|
| 1005 |
def manage_concurrency_limits(self):
|
| 1006 |
"""Manage concurrency limits"""
|
| 1007 |
while True:
|