superqode 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
"""Provider selection dialog with keyboard navigation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from prompt_toolkit import prompt
|
|
6
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.box import ROUNDED
|
|
11
|
+
from getpass import getpass
|
|
12
|
+
|
|
13
|
+
from superqode.providers import ProviderManager, ProviderInfo, ModelInfo
|
|
14
|
+
from superqode.providers.registry import get_free_providers, PROVIDERS
|
|
15
|
+
|
|
16
|
+
_console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProviderCompleter(Completer):
|
|
20
|
+
"""Completer for provider selection."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, providers: List[ProviderInfo]):
|
|
23
|
+
self.providers = providers
|
|
24
|
+
|
|
25
|
+
def get_completions(self, document, complete_event):
|
|
26
|
+
"""Get completions for provider names."""
|
|
27
|
+
text = document.text_before_cursor.lower()
|
|
28
|
+
for idx, provider in enumerate(self.providers, 1):
|
|
29
|
+
if text in provider.name.lower() or text in provider.id.lower() or text == str(idx):
|
|
30
|
+
yield Completion(
|
|
31
|
+
provider.id,
|
|
32
|
+
start_position=-len(text),
|
|
33
|
+
display=f"{idx}. {provider.name}",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CategoryCompleter(Completer):
|
|
38
|
+
"""Completer for category selection."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, categories: List[str]):
|
|
41
|
+
self.categories = categories
|
|
42
|
+
|
|
43
|
+
def get_completions(self, document, complete_event):
|
|
44
|
+
"""Get completions for category names."""
|
|
45
|
+
text = document.text_before_cursor.lower()
|
|
46
|
+
for idx, category in enumerate(self.categories, 1):
|
|
47
|
+
display_name = category.replace("-", " ").title()
|
|
48
|
+
if text in display_name.lower() or text in category.lower() or text == str(idx):
|
|
49
|
+
yield Completion(
|
|
50
|
+
category,
|
|
51
|
+
start_position=-len(text),
|
|
52
|
+
display=f"{idx}. {display_name}",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ModelCompleter(Completer):
|
|
57
|
+
"""Completer for model selection."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, models: List[ModelInfo]):
|
|
60
|
+
self.models = models
|
|
61
|
+
|
|
62
|
+
def get_completions(self, document, complete_event):
|
|
63
|
+
"""Get completions for model names."""
|
|
64
|
+
text = document.text_before_cursor.lower()
|
|
65
|
+
for idx, model in enumerate(self.models, 1):
|
|
66
|
+
if text in model.name.lower() or text in model.id.lower() or text == str(idx):
|
|
67
|
+
yield Completion(
|
|
68
|
+
model.id,
|
|
69
|
+
start_position=-len(text),
|
|
70
|
+
display=f"{idx}. {model.name}",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ProviderDialog:
|
|
75
|
+
"""Dialog for selecting a provider with keyboard navigation."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, manager: Optional[ProviderManager] = None):
|
|
78
|
+
self.manager = manager or ProviderManager()
|
|
79
|
+
self.selected_provider: Optional[ProviderInfo] = None
|
|
80
|
+
|
|
81
|
+
def show(self) -> Optional[str]:
|
|
82
|
+
"""
|
|
83
|
+
Show the provider selection dialog.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Selected provider ID, or None if cancelled
|
|
87
|
+
"""
|
|
88
|
+
providers = self.manager.list_providers()
|
|
89
|
+
|
|
90
|
+
if not providers:
|
|
91
|
+
_console.print(
|
|
92
|
+
"[red]No providers available. Please configure at least one provider.[/red]"
|
|
93
|
+
)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Group providers
|
|
97
|
+
popular_providers = []
|
|
98
|
+
other_providers = []
|
|
99
|
+
chinese_providers = []
|
|
100
|
+
|
|
101
|
+
popular_ids = {"ollama", "anthropic", "github-copilot", "openai", "google", "openrouter"}
|
|
102
|
+
chinese_ids = {
|
|
103
|
+
"qwen",
|
|
104
|
+
"deepseek",
|
|
105
|
+
"zhipu",
|
|
106
|
+
"moonshot",
|
|
107
|
+
"minimax",
|
|
108
|
+
"baidu",
|
|
109
|
+
"tencent",
|
|
110
|
+
"doubao",
|
|
111
|
+
"01-ai",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for provider in providers:
|
|
115
|
+
if provider.id in popular_ids:
|
|
116
|
+
popular_providers.append(provider)
|
|
117
|
+
elif provider.id in chinese_ids:
|
|
118
|
+
chinese_providers.append(provider)
|
|
119
|
+
else:
|
|
120
|
+
other_providers.append(provider)
|
|
121
|
+
|
|
122
|
+
# Flatten for selection
|
|
123
|
+
all_providers = popular_providers + other_providers + chinese_providers
|
|
124
|
+
|
|
125
|
+
# Display provider selection
|
|
126
|
+
_console.print()
|
|
127
|
+
_console.print(
|
|
128
|
+
Panel.fit(
|
|
129
|
+
"[bold cyan]Select Provider[/bold cyan]\n[dim]Type number, provider name, or use Tab to autocomplete[/dim]",
|
|
130
|
+
border_style="bright_cyan",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
_console.print()
|
|
134
|
+
|
|
135
|
+
# Create table for popular providers
|
|
136
|
+
if popular_providers:
|
|
137
|
+
_console.print("[bold bright_yellow]Popular Providers:[/bold bright_yellow]")
|
|
138
|
+
_console.print()
|
|
139
|
+
table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
140
|
+
table.add_column("#", style="dim", width=3)
|
|
141
|
+
table.add_column("Provider", style="cyan", width=25)
|
|
142
|
+
table.add_column("Status", width=20)
|
|
143
|
+
table.add_column("Description", style="dim")
|
|
144
|
+
|
|
145
|
+
for idx, provider in enumerate(popular_providers, 1):
|
|
146
|
+
status = (
|
|
147
|
+
"[green]✓ Configured[/green]"
|
|
148
|
+
if provider.configured
|
|
149
|
+
else "[yellow]⚠ Needs API Key[/yellow]"
|
|
150
|
+
)
|
|
151
|
+
table.add_row(
|
|
152
|
+
str(idx),
|
|
153
|
+
provider.name,
|
|
154
|
+
status,
|
|
155
|
+
provider.description,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
_console.print(table)
|
|
159
|
+
_console.print()
|
|
160
|
+
|
|
161
|
+
# Create table for other providers
|
|
162
|
+
if other_providers:
|
|
163
|
+
_console.print("[bold bright_cyan]Other Providers:[/bold bright_cyan]")
|
|
164
|
+
_console.print()
|
|
165
|
+
table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
166
|
+
table.add_column("#", style="dim", width=3)
|
|
167
|
+
table.add_column("Provider", style="cyan", width=25)
|
|
168
|
+
table.add_column("Status", width=20)
|
|
169
|
+
table.add_column("Description", style="dim")
|
|
170
|
+
|
|
171
|
+
start_idx = len(popular_providers) + 1
|
|
172
|
+
for idx, provider in enumerate(other_providers, start_idx):
|
|
173
|
+
status = (
|
|
174
|
+
"[green]✓ Configured[/green]"
|
|
175
|
+
if provider.configured
|
|
176
|
+
else "[yellow]⚠ Needs API Key[/yellow]"
|
|
177
|
+
)
|
|
178
|
+
table.add_row(
|
|
179
|
+
str(idx),
|
|
180
|
+
provider.name,
|
|
181
|
+
status,
|
|
182
|
+
provider.description,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
_console.print(table)
|
|
186
|
+
_console.print()
|
|
187
|
+
|
|
188
|
+
# Create table for Chinese providers
|
|
189
|
+
if chinese_providers:
|
|
190
|
+
_console.print("[bold bright_red]Chinese Providers:[/bold bright_red]")
|
|
191
|
+
_console.print()
|
|
192
|
+
table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
193
|
+
table.add_column("#", style="dim", width=3)
|
|
194
|
+
table.add_column("Provider", style="cyan", width=25)
|
|
195
|
+
table.add_column("Status", width=20)
|
|
196
|
+
table.add_column("Description", style="dim")
|
|
197
|
+
|
|
198
|
+
start_idx = len(popular_providers) + len(other_providers) + 1
|
|
199
|
+
for idx, provider in enumerate(chinese_providers, start_idx):
|
|
200
|
+
status = (
|
|
201
|
+
"[green]✓ Configured[/green]"
|
|
202
|
+
if provider.configured
|
|
203
|
+
else "[yellow]⚠ Needs API Key[/yellow]"
|
|
204
|
+
)
|
|
205
|
+
table.add_row(
|
|
206
|
+
str(idx),
|
|
207
|
+
provider.name,
|
|
208
|
+
status,
|
|
209
|
+
provider.description,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
_console.print(table)
|
|
213
|
+
_console.print()
|
|
214
|
+
|
|
215
|
+
# Get user selection with autocomplete
|
|
216
|
+
completer = ProviderCompleter(all_providers)
|
|
217
|
+
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
choice = prompt(
|
|
221
|
+
"Select provider (number/name, Tab to autocomplete, 'q' to cancel): ",
|
|
222
|
+
completer=completer,
|
|
223
|
+
).strip()
|
|
224
|
+
|
|
225
|
+
if choice.lower() in ("q", "quit", "exit", ""):
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# Try number selection
|
|
229
|
+
try:
|
|
230
|
+
idx = int(choice) - 1
|
|
231
|
+
if 0 <= idx < len(all_providers):
|
|
232
|
+
selected = all_providers[idx]
|
|
233
|
+
break
|
|
234
|
+
except ValueError:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# Try name/ID selection
|
|
238
|
+
choice_lower = choice.lower()
|
|
239
|
+
for provider in all_providers:
|
|
240
|
+
if choice_lower == provider.id.lower() or choice_lower in provider.name.lower():
|
|
241
|
+
selected = provider
|
|
242
|
+
break
|
|
243
|
+
else:
|
|
244
|
+
_console.print(f"[red]Invalid selection: {choice}[/red]")
|
|
245
|
+
_console.print(
|
|
246
|
+
"[dim]Please enter a number, provider name, or use Tab for autocomplete.[/dim]"
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
self.selected_provider = selected
|
|
256
|
+
|
|
257
|
+
# Show experimental warning for vLLM and SGLang
|
|
258
|
+
if selected.id in ("vllm", "sglang"):
|
|
259
|
+
_console.print()
|
|
260
|
+
_console.print(
|
|
261
|
+
Panel(
|
|
262
|
+
f"[yellow]⚠️ Experimental Provider Warning[/yellow]\n\n"
|
|
263
|
+
f"{selected.name} support is [bold yellow]EXPERIMENTAL[/bold yellow]. "
|
|
264
|
+
f"Features may be unstable and behavior may change.\n\n"
|
|
265
|
+
f"Please report any issues you encounter.",
|
|
266
|
+
border_style="yellow",
|
|
267
|
+
title="Experimental Feature",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
_console.print()
|
|
271
|
+
|
|
272
|
+
# If not configured, prompt for API key
|
|
273
|
+
if not selected.configured and selected.requires_api_key:
|
|
274
|
+
api_key = self._prompt_api_key(selected)
|
|
275
|
+
if api_key is None:
|
|
276
|
+
return None
|
|
277
|
+
# Set the API key in environment for this session
|
|
278
|
+
env_var = self._get_env_var_for_provider(selected.id)
|
|
279
|
+
os.environ[env_var] = api_key
|
|
280
|
+
# Google - also set GEMINI_API_KEY for compatibility
|
|
281
|
+
if selected.id == "google":
|
|
282
|
+
os.environ["GEMINI_API_KEY"] = api_key
|
|
283
|
+
_console.print(f"[green]✓ API key set for {selected.name}[/green]")
|
|
284
|
+
# Re-check configuration
|
|
285
|
+
selected.configured = True
|
|
286
|
+
|
|
287
|
+
# Test connection
|
|
288
|
+
_console.print(f"\n[yellow]Testing connection to {selected.name}...[/yellow]")
|
|
289
|
+
success, error = self.manager.test_connection(selected.id)
|
|
290
|
+
|
|
291
|
+
if not success:
|
|
292
|
+
_console.print(f"[red]Connection failed: {error}[/red]")
|
|
293
|
+
if selected.requires_api_key:
|
|
294
|
+
env_var = self._get_env_var_for_provider(selected.id)
|
|
295
|
+
_console.print(f"\n[dim]To configure {selected.name}, set the API key:[/dim]")
|
|
296
|
+
_console.print(f"[dim] export {env_var}=your-key[/dim]")
|
|
297
|
+
_console.print(f"[dim] or[/dim]")
|
|
298
|
+
_console.print(f"[dim] export CODEOPTIX_LLM_API_KEY=your-key[/dim]")
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
_console.print(f"[green]✓ Selected: {selected.name}[/green]")
|
|
302
|
+
return selected.id
|
|
303
|
+
|
|
304
|
+
def _prompt_api_key(self, provider: ProviderInfo) -> Optional[str]:
|
|
305
|
+
"""Prompt user for API key (like OpenAI does - hidden input)."""
|
|
306
|
+
env_var = self._get_env_var_for_provider(provider.id)
|
|
307
|
+
|
|
308
|
+
_console.print()
|
|
309
|
+
_console.print(
|
|
310
|
+
Panel.fit(
|
|
311
|
+
f"[bold cyan]Configure {provider.name}[/bold cyan]\n\n"
|
|
312
|
+
f"[dim]Please enter your API key for {provider.name}.[/dim]\n"
|
|
313
|
+
f"[dim]This will be set for the current session only.[/dim]\n"
|
|
314
|
+
f"[dim]Environment variable: {env_var}[/dim]\n\n"
|
|
315
|
+
f"[yellow]Note: Your API key will be hidden for security (like password input).[/yellow]",
|
|
316
|
+
border_style="bright_cyan",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
_console.print()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
# Use getpass for security (hides input like password)
|
|
323
|
+
api_key = getpass(f"Enter API key for {provider.name}: ").strip()
|
|
324
|
+
|
|
325
|
+
if not api_key:
|
|
326
|
+
_console.print("[yellow]No API key provided. Cancelled.[/yellow]")
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# Confirm the key (optional, but good UX)
|
|
330
|
+
api_key_confirm = getpass(f"Confirm API key: ").strip()
|
|
331
|
+
|
|
332
|
+
if api_key != api_key_confirm:
|
|
333
|
+
_console.print("[red]API keys do not match. Cancelled.[/red]")
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
return api_key
|
|
337
|
+
except KeyboardInterrupt:
|
|
338
|
+
_console.print("\n[yellow]Cancelled.[/yellow]")
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
def _get_env_var_for_provider(self, provider_id: str) -> str:
|
|
342
|
+
"""Get environment variable name for a provider."""
|
|
343
|
+
env_var_mapping = {
|
|
344
|
+
# US/International Providers
|
|
345
|
+
"openai": "OPENAI_API_KEY",
|
|
346
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
347
|
+
"google": "GOOGLE_API_KEY",
|
|
348
|
+
"xai": "XAI_API_KEY",
|
|
349
|
+
"groq": "GROQ_API_KEY",
|
|
350
|
+
"cerebras": "CEREBRAS_API_KEY",
|
|
351
|
+
"together": "TOGETHER_API_KEY",
|
|
352
|
+
"deepinfra": "DEEPINFRA_API_KEY",
|
|
353
|
+
"github-copilot": "GITHUB_TOKEN",
|
|
354
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
355
|
+
"perplexity": "PERPLEXITY_API_KEY",
|
|
356
|
+
"mistral": "MISTRAL_API_KEY",
|
|
357
|
+
"meta": "META_API_KEY",
|
|
358
|
+
"azure-openai": "AZURE_OPENAI_API_KEY",
|
|
359
|
+
"vertex-ai": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
360
|
+
"openai-compatible": "OPENAI_COMPATIBLE_API_KEY",
|
|
361
|
+
# Chinese Providers
|
|
362
|
+
"qwen": "DASHSCOPE_API_KEY",
|
|
363
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
364
|
+
"zhipu": "ZHIPU_API_KEY",
|
|
365
|
+
"moonshot": "MOONSHOT_API_KEY",
|
|
366
|
+
"minimax": "MINIMAX_API_KEY",
|
|
367
|
+
"baidu": "BAIDU_API_KEY",
|
|
368
|
+
"tencent": "TENCENT_API_KEY",
|
|
369
|
+
"doubao": "DOUBAO_API_KEY",
|
|
370
|
+
"01-ai": "ZEROONE_API_KEY",
|
|
371
|
+
# Legacy mappings for backward compatibility
|
|
372
|
+
"together-ai": "TOGETHER_API_KEY",
|
|
373
|
+
"google-vertex": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
374
|
+
"azure": "AZURE_OPENAI_API_KEY",
|
|
375
|
+
"cohere": "COHERE_API_KEY",
|
|
376
|
+
"amazon-bedrock": "AWS_ACCESS_KEY_ID",
|
|
377
|
+
"gateway": "GATEWAY_API_KEY",
|
|
378
|
+
}
|
|
379
|
+
return env_var_mapping.get(provider_id, f"{provider_id.upper()}_API_KEY")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ConnectDialog:
|
|
383
|
+
"""Modal dialog for connecting to LLM providers with category selection."""
|
|
384
|
+
|
|
385
|
+
# Provider categories
|
|
386
|
+
CATEGORIES = {
|
|
387
|
+
"us-labs": {
|
|
388
|
+
"name": "[bright_blue]US Labs[/bright_blue]",
|
|
389
|
+
"description": "Premium models from leading US AI companies",
|
|
390
|
+
"providers": ["openai", "anthropic", "google", "xai", "amazon-bedrock"],
|
|
391
|
+
},
|
|
392
|
+
"china-labs": {
|
|
393
|
+
"name": "[bright_red]China Labs[/bright_red]",
|
|
394
|
+
"description": "Models from Chinese AI companies",
|
|
395
|
+
"providers": [
|
|
396
|
+
"deepseek",
|
|
397
|
+
"qwen",
|
|
398
|
+
"zhipu",
|
|
399
|
+
"moonshot",
|
|
400
|
+
"minimax",
|
|
401
|
+
"baidu",
|
|
402
|
+
"tencent",
|
|
403
|
+
"doubao",
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
"other-labs": {
|
|
407
|
+
"name": "[bright_green]Other Labs[/bright_green]",
|
|
408
|
+
"description": "Labs from other countries with their own models",
|
|
409
|
+
"providers": ["mistral"],
|
|
410
|
+
},
|
|
411
|
+
"model-hosts": {
|
|
412
|
+
"name": "[bright_magenta]Model Hosts[/bright_magenta]",
|
|
413
|
+
"description": "Services hosting many open and proprietary models",
|
|
414
|
+
"providers": [
|
|
415
|
+
"openrouter",
|
|
416
|
+
"together",
|
|
417
|
+
"groq",
|
|
418
|
+
"fireworks",
|
|
419
|
+
"huggingface",
|
|
420
|
+
"cerebras",
|
|
421
|
+
"perplexity",
|
|
422
|
+
"cohere",
|
|
423
|
+
"opencode",
|
|
424
|
+
"github-copilot",
|
|
425
|
+
"azure",
|
|
426
|
+
"vertex",
|
|
427
|
+
"cloudflare",
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
"local": {
|
|
431
|
+
"name": "[bright_cyan]Local & Self-Hosted[/bright_cyan]",
|
|
432
|
+
"description": "Local engines and OpenAI-compatible self-hosted endpoints",
|
|
433
|
+
"providers": [
|
|
434
|
+
"ollama",
|
|
435
|
+
"lmstudio",
|
|
436
|
+
"mlx",
|
|
437
|
+
"vllm",
|
|
438
|
+
"sglang",
|
|
439
|
+
"tgi",
|
|
440
|
+
"huggingface",
|
|
441
|
+
"openai-compatible",
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
"free-models": {
|
|
445
|
+
"name": "[bright_yellow]🆓 Free Models[/bright_yellow]",
|
|
446
|
+
"description": "Providers offering free models or free tiers",
|
|
447
|
+
"providers": [], # Will be populated dynamically
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
def __init__(self, manager: Optional[ProviderManager] = None):
|
|
452
|
+
self.manager = manager or ProviderManager()
|
|
453
|
+
self.selected_provider: Optional[ProviderInfo] = None
|
|
454
|
+
self.selected_model: Optional[ModelInfo] = None
|
|
455
|
+
# Dynamically populate free-models category
|
|
456
|
+
self._populate_free_models_category()
|
|
457
|
+
|
|
458
|
+
def _populate_free_models_category(self):
|
|
459
|
+
"""Dynamically populate the free-models category from registry."""
|
|
460
|
+
free_providers = get_free_providers()
|
|
461
|
+
# Get provider IDs that have free models
|
|
462
|
+
free_provider_ids = list(free_providers.keys())
|
|
463
|
+
# Update the category
|
|
464
|
+
if "free-models" in self.CATEGORIES:
|
|
465
|
+
self.CATEGORIES["free-models"]["providers"] = free_provider_ids
|
|
466
|
+
|
|
467
|
+
def show(self) -> Optional[tuple[str, str]]:
|
|
468
|
+
"""
|
|
469
|
+
Show the connect dialog with category selection.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Tuple of (provider_id, model_id) or None if cancelled
|
|
473
|
+
"""
|
|
474
|
+
while True:
|
|
475
|
+
category = self._show_category_selection()
|
|
476
|
+
if category is None:
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
provider_id = self._show_provider_selection(category)
|
|
480
|
+
if provider_id is None:
|
|
481
|
+
continue # Go back to category selection
|
|
482
|
+
|
|
483
|
+
model_id = self._show_model_selection(provider_id)
|
|
484
|
+
if model_id is None:
|
|
485
|
+
continue # Go back to provider selection
|
|
486
|
+
|
|
487
|
+
return (provider_id, model_id)
|
|
488
|
+
|
|
489
|
+
def _show_category_selection(self) -> Optional[str]:
|
|
490
|
+
"""Show category selection modal."""
|
|
491
|
+
_console.print()
|
|
492
|
+
_console.print(
|
|
493
|
+
Panel.fit(
|
|
494
|
+
"[bold bright_blue]🔗 SuperQode Connect[/bold bright_blue]\n"
|
|
495
|
+
"[dim]Choose a category to browse available providers and models[/dim]",
|
|
496
|
+
border_style="bright_blue",
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
_console.print()
|
|
500
|
+
|
|
501
|
+
# Display categories
|
|
502
|
+
table = Table(show_header=True, header_style="bold magenta", box=ROUNDED)
|
|
503
|
+
table.add_column("#", style="dim cyan", width=3, justify="center")
|
|
504
|
+
table.add_column("Category", style="bold white", width=25)
|
|
505
|
+
table.add_column("Description", style="dim")
|
|
506
|
+
table.add_column("Providers", style="yellow", justify="center")
|
|
507
|
+
|
|
508
|
+
for idx, (category_id, category_info) in enumerate(self.CATEGORIES.items(), 1):
|
|
509
|
+
# Count configured providers in this category
|
|
510
|
+
providers = self.manager.list_providers()
|
|
511
|
+
configured_count = sum(
|
|
512
|
+
1 for p in providers if p.id in category_info["providers"] and p.configured
|
|
513
|
+
)
|
|
514
|
+
total_count = len(category_info["providers"])
|
|
515
|
+
|
|
516
|
+
table.add_row(
|
|
517
|
+
str(idx),
|
|
518
|
+
category_info["name"],
|
|
519
|
+
category_info["description"],
|
|
520
|
+
f"{configured_count}/{total_count} configured",
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
_console.print(table)
|
|
524
|
+
_console.print()
|
|
525
|
+
|
|
526
|
+
# Get selection
|
|
527
|
+
while True:
|
|
528
|
+
try:
|
|
529
|
+
choice = prompt(
|
|
530
|
+
"Select category (number, 'q' to cancel): ",
|
|
531
|
+
completer=CategoryCompleter(list(self.CATEGORIES.keys())),
|
|
532
|
+
).strip()
|
|
533
|
+
|
|
534
|
+
if choice.lower() in ("q", "quit", "exit", ""):
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
# Try number selection
|
|
538
|
+
try:
|
|
539
|
+
idx = int(choice) - 1
|
|
540
|
+
if 0 <= idx < len(self.CATEGORIES):
|
|
541
|
+
return list(self.CATEGORIES.keys())[idx]
|
|
542
|
+
except ValueError:
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
# Try name selection
|
|
546
|
+
choice_lower = choice.lower().replace(" ", "-")
|
|
547
|
+
if choice_lower in self.CATEGORIES:
|
|
548
|
+
return choice_lower
|
|
549
|
+
|
|
550
|
+
_console.print(f"[red]Invalid selection: {choice}[/red]")
|
|
551
|
+
_console.print("[dim]Please enter a number or category name.[/dim]")
|
|
552
|
+
|
|
553
|
+
except KeyboardInterrupt:
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
def _show_provider_selection(self, category_id: str) -> Optional[str]:
|
|
557
|
+
"""Show provider selection for the chosen category."""
|
|
558
|
+
category_info = self.CATEGORIES[category_id]
|
|
559
|
+
providers = self.manager.list_providers()
|
|
560
|
+
|
|
561
|
+
# Filter providers for this category
|
|
562
|
+
category_providers = [p for p in providers if p.id in category_info["providers"]]
|
|
563
|
+
|
|
564
|
+
if not category_providers:
|
|
565
|
+
_console.print(f"[red]No providers available in {category_info['name']}[/red]")
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
_console.print()
|
|
569
|
+
_console.print(
|
|
570
|
+
Panel.fit(
|
|
571
|
+
f"[bold bright_green]{category_info['name']}[/bold bright_green]\n"
|
|
572
|
+
f"[dim]{category_info['description']}[/dim]",
|
|
573
|
+
border_style="bright_green",
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
_console.print()
|
|
577
|
+
|
|
578
|
+
# Display providers
|
|
579
|
+
table = Table(show_header=True, header_style="bold magenta", box=ROUNDED)
|
|
580
|
+
table.add_column("#", style="dim cyan", width=3, justify="center")
|
|
581
|
+
table.add_column("Provider", style="bold white", width=25)
|
|
582
|
+
table.add_column("Status", width=20)
|
|
583
|
+
table.add_column("Models", style="yellow", justify="center")
|
|
584
|
+
table.add_column("Description", style="dim")
|
|
585
|
+
|
|
586
|
+
# Get list of providers with free models for badge display
|
|
587
|
+
free_provider_ids = set(get_free_providers().keys())
|
|
588
|
+
|
|
589
|
+
for idx, provider in enumerate(category_providers, 1):
|
|
590
|
+
status = (
|
|
591
|
+
"[green]✓ Configured[/green]"
|
|
592
|
+
if provider.configured
|
|
593
|
+
else "[yellow]⚠ Needs Setup[/yellow]"
|
|
594
|
+
)
|
|
595
|
+
model_count = len(provider.models) if provider.models else 0
|
|
596
|
+
|
|
597
|
+
# Add free badge if provider offers free models
|
|
598
|
+
provider_name = provider.name
|
|
599
|
+
if provider.id in free_provider_ids:
|
|
600
|
+
provider_name = f"{provider.name} [bright_yellow]🆓 Free[/bright_yellow]"
|
|
601
|
+
|
|
602
|
+
table.add_row(str(idx), provider_name, status, str(model_count), provider.description)
|
|
603
|
+
|
|
604
|
+
_console.print(table)
|
|
605
|
+
_console.print()
|
|
606
|
+
|
|
607
|
+
# Get selection
|
|
608
|
+
while True:
|
|
609
|
+
try:
|
|
610
|
+
choice = prompt(
|
|
611
|
+
"Select provider (number/name, 'back' for categories, 'q' to cancel): ",
|
|
612
|
+
completer=ProviderCompleter(category_providers),
|
|
613
|
+
).strip()
|
|
614
|
+
|
|
615
|
+
if choice.lower() in ("q", "quit", "exit", ""):
|
|
616
|
+
return None
|
|
617
|
+
if choice.lower() == "back":
|
|
618
|
+
return None # Go back to category selection
|
|
619
|
+
|
|
620
|
+
# Try number selection
|
|
621
|
+
try:
|
|
622
|
+
idx = int(choice) - 1
|
|
623
|
+
if 0 <= idx < len(category_providers):
|
|
624
|
+
selected = category_providers[idx]
|
|
625
|
+
break
|
|
626
|
+
except ValueError:
|
|
627
|
+
pass
|
|
628
|
+
|
|
629
|
+
# Try name/ID selection
|
|
630
|
+
choice_lower = choice.lower()
|
|
631
|
+
for provider in category_providers:
|
|
632
|
+
if choice_lower == provider.id.lower() or choice_lower in provider.name.lower():
|
|
633
|
+
selected = provider
|
|
634
|
+
break
|
|
635
|
+
else:
|
|
636
|
+
_console.print(f"[red]Invalid selection: {choice}[/red]")
|
|
637
|
+
continue
|
|
638
|
+
|
|
639
|
+
break
|
|
640
|
+
|
|
641
|
+
except KeyboardInterrupt:
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
self.selected_provider = selected
|
|
645
|
+
|
|
646
|
+
# Show experimental warning for vLLM and SGLang
|
|
647
|
+
if selected.id in ("vllm", "sglang"):
|
|
648
|
+
_console.print()
|
|
649
|
+
_console.print(
|
|
650
|
+
Panel(
|
|
651
|
+
f"[yellow]⚠️ Experimental Provider Warning[/yellow]\n\n"
|
|
652
|
+
f"{selected.name} support is [bold yellow]EXPERIMENTAL[/bold yellow]. "
|
|
653
|
+
f"Features may be unstable and behavior may change.\n\n"
|
|
654
|
+
f"Please report any issues you encounter.",
|
|
655
|
+
border_style="yellow",
|
|
656
|
+
title="Experimental Feature",
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
_console.print()
|
|
660
|
+
|
|
661
|
+
# Show available models first (before asking for API keys)
|
|
662
|
+
models = self.manager.get_models(selected.id)
|
|
663
|
+
if models:
|
|
664
|
+
_console.print(f"\n[bold cyan]Available models for {selected.name}:[/bold cyan]")
|
|
665
|
+
for i, model in enumerate(models[:5], 1): # Show first 5 models
|
|
666
|
+
_console.print(f" {i}. {model.name}")
|
|
667
|
+
if len(models) > 5:
|
|
668
|
+
_console.print(f" ... and {len(models) - 5} more models")
|
|
669
|
+
_console.print()
|
|
670
|
+
|
|
671
|
+
# Handle configuration if needed
|
|
672
|
+
if not selected.configured and selected.requires_api_key:
|
|
673
|
+
_console.print(f"[yellow]⚠ {selected.name} requires API key configuration[/yellow]")
|
|
674
|
+
if not self._configure_provider(selected):
|
|
675
|
+
return None # Configuration failed
|
|
676
|
+
|
|
677
|
+
return selected.id
|
|
678
|
+
|
|
679
|
+
def _show_model_selection(self, provider_id: str) -> Optional[str]:
|
|
680
|
+
"""Show model selection for the chosen provider."""
|
|
681
|
+
if not self.selected_provider:
|
|
682
|
+
return None
|
|
683
|
+
|
|
684
|
+
models = self.manager.get_models(provider_id)
|
|
685
|
+
if not models:
|
|
686
|
+
_console.print(
|
|
687
|
+
f"[yellow]No models available for {self.selected_provider.name}[/yellow]"
|
|
688
|
+
)
|
|
689
|
+
_console.print("[dim]This provider may require additional setup or API access.[/dim]")
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
_console.print()
|
|
693
|
+
_console.print(
|
|
694
|
+
Panel.fit(
|
|
695
|
+
f"[bold bright_magenta]{self.selected_provider.name} Models[/bold bright_magenta]",
|
|
696
|
+
border_style="bright_magenta",
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
_console.print()
|
|
700
|
+
|
|
701
|
+
# Display models
|
|
702
|
+
table = Table(show_header=True, header_style="bold cyan", box=ROUNDED)
|
|
703
|
+
table.add_column("#", style="dim cyan", width=3, justify="center")
|
|
704
|
+
table.add_column("Model", style="bold white", width=30)
|
|
705
|
+
table.add_column("Context", style="yellow", justify="right", width=10)
|
|
706
|
+
table.add_column("Status", width=15)
|
|
707
|
+
|
|
708
|
+
for idx, model in enumerate(models[:20], 1): # Limit to first 20 models
|
|
709
|
+
# Model status (experimental, deprecated, etc.)
|
|
710
|
+
status = "[green]active[/green]"
|
|
711
|
+
if hasattr(model, "status") and model.status:
|
|
712
|
+
if model.status == "experimental":
|
|
713
|
+
status = "[yellow]experimental[/yellow]"
|
|
714
|
+
elif model.status == "deprecated":
|
|
715
|
+
status = "[red]deprecated[/red]"
|
|
716
|
+
|
|
717
|
+
table.add_row(
|
|
718
|
+
str(idx),
|
|
719
|
+
model.name,
|
|
720
|
+
f"{model.context_size:,}" if model.context_size else "unknown",
|
|
721
|
+
status,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
_console.print(table)
|
|
725
|
+
|
|
726
|
+
if len(models) > 20:
|
|
727
|
+
_console.print(f"[dim]... and {len(models) - 20} more models[/dim]")
|
|
728
|
+
|
|
729
|
+
_console.print()
|
|
730
|
+
|
|
731
|
+
# Get selection
|
|
732
|
+
while True:
|
|
733
|
+
try:
|
|
734
|
+
choice = prompt(
|
|
735
|
+
"Select model (number/name, 'back' for providers, 'q' to cancel): ",
|
|
736
|
+
completer=ModelCompleter(models),
|
|
737
|
+
).strip()
|
|
738
|
+
|
|
739
|
+
if choice.lower() in ("q", "quit", "exit", ""):
|
|
740
|
+
return None
|
|
741
|
+
if choice.lower() == "back":
|
|
742
|
+
return None # Go back to provider selection
|
|
743
|
+
|
|
744
|
+
# Try number selection
|
|
745
|
+
try:
|
|
746
|
+
idx = int(choice) - 1
|
|
747
|
+
if 0 <= idx < len(models):
|
|
748
|
+
selected = models[idx]
|
|
749
|
+
break
|
|
750
|
+
except ValueError:
|
|
751
|
+
pass
|
|
752
|
+
|
|
753
|
+
# Try name selection
|
|
754
|
+
choice_lower = choice.lower()
|
|
755
|
+
for model in models:
|
|
756
|
+
if choice_lower == model.id.lower() or choice_lower in model.name.lower():
|
|
757
|
+
selected = model
|
|
758
|
+
break
|
|
759
|
+
else:
|
|
760
|
+
_console.print(f"[red]Invalid selection: {choice}[/red]")
|
|
761
|
+
continue
|
|
762
|
+
|
|
763
|
+
break
|
|
764
|
+
|
|
765
|
+
except KeyboardInterrupt:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
self.selected_model = selected
|
|
769
|
+
return selected.id
|
|
770
|
+
|
|
771
|
+
def _configure_provider(self, provider: ProviderInfo) -> bool:
|
|
772
|
+
"""Configure a provider that needs API key setup."""
|
|
773
|
+
api_key = self._prompt_api_key(provider)
|
|
774
|
+
if api_key is None:
|
|
775
|
+
return False
|
|
776
|
+
|
|
777
|
+
# Set the API key in environment
|
|
778
|
+
env_var = self._get_env_var_for_provider(provider.id)
|
|
779
|
+
os.environ[env_var] = api_key
|
|
780
|
+
# Google - also set GEMINI_API_KEY for compatibility
|
|
781
|
+
if provider.id == "google":
|
|
782
|
+
os.environ["GEMINI_API_KEY"] = api_key
|
|
783
|
+
|
|
784
|
+
# Test connection
|
|
785
|
+
_console.print(f"\n[yellow]Testing connection to {provider.name}...[/yellow]")
|
|
786
|
+
success, error = self.manager.test_connection(provider.id)
|
|
787
|
+
|
|
788
|
+
if not success:
|
|
789
|
+
_console.print(f"[red]Connection failed: {error}[/red]")
|
|
790
|
+
_console.print(f"\n[dim]To configure {provider.name} later, set:[/dim]")
|
|
791
|
+
_console.print(f"[dim] export {env_var}=your-key[/dim]")
|
|
792
|
+
return False
|
|
793
|
+
|
|
794
|
+
_console.print(f"[green]✓ Successfully configured {provider.name}[/green]")
|
|
795
|
+
provider.configured = True
|
|
796
|
+
return True
|
|
797
|
+
|
|
798
|
+
def _prompt_api_key(self, provider: ProviderInfo) -> Optional[str]:
|
|
799
|
+
"""Prompt user for API key."""
|
|
800
|
+
env_var = self._get_env_var_for_provider(provider.id)
|
|
801
|
+
|
|
802
|
+
_console.print()
|
|
803
|
+
_console.print(
|
|
804
|
+
Panel.fit(
|
|
805
|
+
f"[bold cyan]🔑 Configure {provider.name}[/bold cyan]\n\n"
|
|
806
|
+
f"[dim]{provider.description}[/dim]\n\n"
|
|
807
|
+
f"[yellow]API Key Required[/yellow]\n"
|
|
808
|
+
f"[dim]Environment variable: {env_var}[/dim]\n\n"
|
|
809
|
+
f"[yellow]Note: Your API key will be hidden for security.[/yellow]",
|
|
810
|
+
border_style="bright_cyan",
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
_console.print()
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
api_key = getpass(f"Enter API key for {provider.name}: ").strip()
|
|
817
|
+
|
|
818
|
+
if not api_key:
|
|
819
|
+
_console.print("[yellow]No API key provided. Cancelled.[/yellow]")
|
|
820
|
+
return None
|
|
821
|
+
|
|
822
|
+
# Optional confirmation
|
|
823
|
+
confirm = prompt("Confirm API key? (y/n): ", default="y").strip().lower()
|
|
824
|
+
if confirm not in ("y", "yes", ""):
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
return api_key
|
|
828
|
+
except KeyboardInterrupt:
|
|
829
|
+
_console.print("\n[yellow]Cancelled.[/yellow]")
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
def _get_env_var_for_provider(self, provider_id: str) -> str:
|
|
833
|
+
"""Get environment variable name for a provider."""
|
|
834
|
+
env_var_mapping = {
|
|
835
|
+
# US/International Providers
|
|
836
|
+
"openai": "OPENAI_API_KEY",
|
|
837
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
838
|
+
"google": "GOOGLE_API_KEY",
|
|
839
|
+
"xai": "XAI_API_KEY",
|
|
840
|
+
"groq": "GROQ_API_KEY",
|
|
841
|
+
"cerebras": "CEREBRAS_API_KEY",
|
|
842
|
+
"together": "TOGETHER_API_KEY",
|
|
843
|
+
"deepinfra": "DEEPINFRA_API_KEY",
|
|
844
|
+
"github-copilot": "GITHUB_TOKEN",
|
|
845
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
846
|
+
"perplexity": "PERPLEXITY_API_KEY",
|
|
847
|
+
"mistral": "MISTRAL_API_KEY",
|
|
848
|
+
"meta": "META_API_KEY",
|
|
849
|
+
"azure-openai": "AZURE_OPENAI_API_KEY",
|
|
850
|
+
"vertex-ai": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
851
|
+
"openai-compatible": "OPENAI_COMPATIBLE_API_KEY",
|
|
852
|
+
# Chinese Providers
|
|
853
|
+
"qwen": "DASHSCOPE_API_KEY",
|
|
854
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
855
|
+
"zhipu": "ZHIPU_API_KEY",
|
|
856
|
+
"moonshot": "MOONSHOT_API_KEY",
|
|
857
|
+
"minimax": "MINIMAX_API_KEY",
|
|
858
|
+
"baidu": "BAIDU_API_KEY",
|
|
859
|
+
"tencent": "TENCENT_API_KEY",
|
|
860
|
+
"doubao": "DOUBAO_API_KEY",
|
|
861
|
+
"01-ai": "ZEROONE_API_KEY",
|
|
862
|
+
# Legacy mappings for backward compatibility
|
|
863
|
+
"together-ai": "TOGETHER_API_KEY",
|
|
864
|
+
"google-vertex": "GOOGLE_APPLICATION_CREDENTIALS",
|
|
865
|
+
"azure": "AZURE_OPENAI_API_KEY",
|
|
866
|
+
"cohere": "COHERE_API_KEY",
|
|
867
|
+
"amazon-bedrock": "AWS_ACCESS_KEY_ID",
|
|
868
|
+
"gateway": "GATEWAY_API_KEY",
|
|
869
|
+
}
|
|
870
|
+
return env_var_mapping.get(provider_id, f"{provider_id.upper()}_API_KEY")
|