fix: ask for quick start model id
Remove automatic Quick Start model discovery. Users now explicitly enter the model ID after choosing the provider and API key, and incomplete Quick Start input does not leave partial provider config behind.
This commit is contained in:
@@ -156,7 +156,7 @@ You will see a menu like this:
|
||||
|
||||
```text
|
||||
> What would you like to do?
|
||||
[Q] Quick Start (provider + key)
|
||||
[Q] Quick Start (provider + key + model)
|
||||
[A] Advanced Settings
|
||||
[X] Exit
|
||||
```
|
||||
@@ -169,15 +169,17 @@ Move through the wizard like this:
|
||||
| The provider menu | Choose the company or service that issued your API key. |
|
||||
| The API key field | Paste the key, then press `Enter`. |
|
||||
| A base URL field for `Other OpenAI-compatible` | Paste the provider base URL from its docs, then press `Enter`. |
|
||||
| The Model ID field | Paste a model name from your provider, then press `Enter`. |
|
||||
| A back option in Advanced Settings | Choose it to return to the previous menu. |
|
||||
|
||||
For the first setup, choose `[Q] Quick Start (provider + key)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a provider that is not in the Quick Start menu, a chat app, or a tool setup.
|
||||
For the first setup, choose `[Q] Quick Start (provider + key + model)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a provider that is not in the Quick Start menu, a chat app, or a tool setup.
|
||||
|
||||
1. Choose `[Q] Quick Start (provider + key)`.
|
||||
1. Choose `[Q] Quick Start (provider + key + model)`.
|
||||
2. Choose the provider that issued your API key.
|
||||
3. Paste your API key.
|
||||
4. If you chose `Other OpenAI-compatible`, paste the provider base URL from that provider's docs.
|
||||
5. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||
5. Paste a model ID that provider can run.
|
||||
6. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||
|
||||
The recommended path enables the local WebUI and default AI settings. You do not need to choose a chat channel for the first run.
|
||||
|
||||
|
||||
+10
-52
@@ -65,11 +65,9 @@ _QUICK_START_PROVIDER_KEYS = (
|
||||
"zhipu",
|
||||
)
|
||||
_QUICK_START_CUSTOM_PROVIDER_CHOICE = "Other OpenAI-compatible"
|
||||
_QUICK_START_MODEL_FETCH_API_BASES = {
|
||||
"openai": "https://api.openai.com/v1",
|
||||
}
|
||||
|
||||
_QUICK_START_STEPS = ("Provider + key", "WebUI", "Review")
|
||||
_QUICK_START_MENU_CHOICE = "[Q] Quick Start (provider + key + model)"
|
||||
_QUICK_START_STEPS = ("Provider + model", "WebUI", "Review")
|
||||
|
||||
# Low-contrast terminal palette inspired by JetBrains Darcula/Islands.
|
||||
_UI_ACCENT = "#6B9BFA"
|
||||
@@ -400,7 +398,7 @@ def _show_main_menu_header() -> None:
|
||||
body = Table.grid(expand=True)
|
||||
body.add_column(ratio=1)
|
||||
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start asks for the provider and API key.[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start asks for the provider, API key, and model.[/]")
|
||||
body.add_row(
|
||||
f"[{_UI_MUTED}]Use Advanced later for other providers or chat apps.[/]"
|
||||
)
|
||||
@@ -1434,39 +1432,6 @@ def _get_quick_start_provider_choices() -> dict[str, str]:
|
||||
return choices
|
||||
|
||||
|
||||
def _models_url(api_base: str) -> str:
|
||||
"""Return the OpenAI-compatible models endpoint for a base URL."""
|
||||
return f"{api_base.rstrip('/')}/models"
|
||||
|
||||
|
||||
def _fetch_first_quick_start_model(api_base: str, api_key: str) -> str | None:
|
||||
"""Fetch the first model ID from a user-approved OpenAI-compatible base URL."""
|
||||
import httpx
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
_models_url(api_base),
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=8.0,
|
||||
follow_redirects=True,
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
return None
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
if not isinstance(data, list):
|
||||
return None
|
||||
for item in data:
|
||||
if isinstance(item, dict) and isinstance(item.get("id"), str) and item["id"].strip():
|
||||
return item["id"].strip()
|
||||
return None
|
||||
|
||||
|
||||
def _configure_quick_start_provider(config: Config) -> bool:
|
||||
"""Configure the beginner path from provider + API key."""
|
||||
_show_quick_start_progress(1)
|
||||
@@ -1508,23 +1473,16 @@ def _configure_quick_start_provider(config: Config) -> bool:
|
||||
console.print(f"[red]Unknown provider: {provider_name}[/red]")
|
||||
return False
|
||||
|
||||
provider_config.api_key = api_key
|
||||
if api_base and not provider_config.api_base:
|
||||
provider_config.api_base = api_base
|
||||
|
||||
model = None
|
||||
model_api_base = provider_config.api_base or _QUICK_START_MODEL_FETCH_API_BASES.get(
|
||||
provider_name
|
||||
)
|
||||
if model_api_base:
|
||||
model = _fetch_first_quick_start_model(model_api_base, api_key)
|
||||
if not model:
|
||||
model = _input_model_with_autocomplete("Model ID", "", provider_name)
|
||||
model = (model or "").strip()
|
||||
if not model:
|
||||
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
|
||||
return False
|
||||
|
||||
provider_config.api_key = api_key
|
||||
if api_base and not provider_config.api_base:
|
||||
provider_config.api_base = api_base
|
||||
|
||||
_set_primary_quick_start_preset(
|
||||
config,
|
||||
provider_name,
|
||||
@@ -1582,7 +1540,7 @@ def _configure_quick_start(config: Config) -> bool:
|
||||
console.clear()
|
||||
_show_section_header(
|
||||
"Quick Start",
|
||||
"Choose the API provider, paste the key, then use the local WebUI.",
|
||||
"Choose the API provider, paste the key, enter the model, then use the local WebUI.",
|
||||
)
|
||||
if not _configure_quick_start_provider(config):
|
||||
_pause()
|
||||
@@ -1629,7 +1587,7 @@ def _prompt_main_menu_exit(has_unsaved_changes: bool) -> str:
|
||||
def _get_main_menu_choices(has_unsaved_changes: bool) -> list[str]:
|
||||
"""Return the top-level choices, keeping save actions hidden until needed."""
|
||||
choices = [
|
||||
"[Q] Quick Start (provider + key)",
|
||||
_QUICK_START_MENU_CHOICE,
|
||||
"[A] Advanced Settings",
|
||||
]
|
||||
if has_unsaved_changes:
|
||||
@@ -1734,7 +1692,7 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
return OnboardResult(config=original_config, should_save=False)
|
||||
continue
|
||||
|
||||
if answer == "[Q] Quick Start (provider + key)":
|
||||
if answer == _QUICK_START_MENU_CHOICE:
|
||||
if _configure_quick_start(config):
|
||||
return OnboardResult(config=config, should_save=True)
|
||||
continue
|
||||
|
||||
@@ -866,7 +866,7 @@ class TestMainMenuUpdate:
|
||||
dirty_choices = _get_main_menu_choices(True)
|
||||
|
||||
assert clean_choices == [
|
||||
"[Q] Quick Start (provider + key)",
|
||||
"[Q] Quick Start (provider + key + model)",
|
||||
"[A] Advanced Settings",
|
||||
"[X] Exit",
|
||||
]
|
||||
@@ -880,7 +880,7 @@ class TestMainMenuUpdate:
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[Q] Quick Start (provider + key)",
|
||||
"[Q] Quick Start (provider + key + model)",
|
||||
])
|
||||
|
||||
class FakePrompt:
|
||||
@@ -907,12 +907,9 @@ class TestMainMenuUpdate:
|
||||
assert result.config.agents.defaults.bot_name == "quickbot"
|
||||
|
||||
def test_quick_start_provider_choice_skips_advanced_prompts(self, monkeypatch):
|
||||
"""The beginner path should ask for provider and API key without advanced settings."""
|
||||
"""The beginner path should ask for provider, API key, and model."""
|
||||
config = Config()
|
||||
|
||||
def fail_model_input(*_args, **_kwargs):
|
||||
raise AssertionError("Quick Start should not ask for a model ID when /models works")
|
||||
|
||||
def fail_websocket_config(*_args, **_kwargs):
|
||||
raise AssertionError("Quick Start should not open WebSocket settings")
|
||||
|
||||
@@ -922,8 +919,11 @@ class TestMainMenuUpdate:
|
||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "sk-ds-test")
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", lambda *a, **kw: "deepseek-v4-flash")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_input_model_with_autocomplete",
|
||||
lambda *a, **kw: "deepseek-v4-flash",
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
|
||||
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
|
||||
@@ -940,70 +940,53 @@ class TestMainMenuUpdate:
|
||||
assert websocket["enabled"] is True
|
||||
assert websocket["websocketRequiresToken"] is True
|
||||
|
||||
def test_quick_start_provider_choice_fetches_models_from_selected_provider(self, monkeypatch):
|
||||
"""Known providers should fetch models only from the selected provider base URL."""
|
||||
def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch):
|
||||
"""Known providers should ask users for the model instead of fetching one."""
|
||||
config = Config()
|
||||
calls: dict[str, str] = {}
|
||||
model_prompts: list[tuple[str, str, str]] = []
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "OpenRouter")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "sk-or-test")
|
||||
|
||||
def fake_fetch(api_base, api_key):
|
||||
calls["api_base"] = api_base
|
||||
calls["api_key"] = api_key
|
||||
def fake_model_input(prompt, current, provider):
|
||||
model_prompts.append((prompt, current, provider))
|
||||
return "openai/gpt-4o-mini"
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", fake_fetch)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fake_model_input)
|
||||
|
||||
assert onboard_wizard._configure_quick_start_provider(config) is True
|
||||
|
||||
assert calls == {
|
||||
"api_base": "https://openrouter.ai/api/v1",
|
||||
"api_key": "sk-or-test",
|
||||
}
|
||||
assert model_prompts == [("Model ID", "", "openrouter")]
|
||||
assert config.providers.openrouter.api_key == "sk-or-test"
|
||||
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
||||
assert config.model_presets["primary"].provider == "openrouter"
|
||||
assert config.model_presets["primary"].model == "openai/gpt-4o-mini"
|
||||
|
||||
def test_quick_start_openai_fetches_models_without_storing_base(self, monkeypatch):
|
||||
"""OpenAI should still support key-only setup via its SDK default endpoint."""
|
||||
def test_quick_start_openai_stores_key_and_model_without_base(self, monkeypatch):
|
||||
"""OpenAI should support key-only setup without storing a default base URL."""
|
||||
config = Config()
|
||||
calls: dict[str, str] = {}
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "OpenAI")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "sk-openai-test")
|
||||
|
||||
def fake_fetch(api_base, api_key):
|
||||
calls["api_base"] = api_base
|
||||
calls["api_key"] = api_key
|
||||
return "gpt-4o-mini"
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", fake_fetch)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_input_model_with_autocomplete",
|
||||
lambda *a, **kw: "gpt-4o-mini",
|
||||
)
|
||||
|
||||
assert onboard_wizard._configure_quick_start_provider(config) is True
|
||||
|
||||
assert calls == {
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"api_key": "sk-openai-test",
|
||||
}
|
||||
assert config.providers.openai.api_key == "sk-openai-test"
|
||||
assert config.providers.openai.api_base is None
|
||||
assert config.model_presets["primary"].provider == "openai"
|
||||
assert config.model_presets["primary"].model == "gpt-4o-mini"
|
||||
|
||||
def test_quick_start_custom_base_url_fetches_first_model(self, monkeypatch):
|
||||
"""Unknown providers should use only the user-provided base URL to fetch models."""
|
||||
def test_quick_start_custom_base_url_asks_for_model_id(self, monkeypatch):
|
||||
"""Custom providers should ask for base URL and model ID."""
|
||||
config = Config()
|
||||
text_answers = iter(["sk-custom-test", "https://api.example.test/v1"])
|
||||
calls: dict[str, str] = {}
|
||||
|
||||
def fake_fetch(api_base, api_key):
|
||||
calls["api_base"] = api_base
|
||||
calls["api_key"] = api_key
|
||||
return "custom-model"
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(
|
||||
@@ -1012,11 +995,14 @@ class TestMainMenuUpdate:
|
||||
lambda *a, **kw: onboard_wizard._QUICK_START_CUSTOM_PROVIDER_CHOICE,
|
||||
)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: next(text_answers))
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", fake_fetch)
|
||||
monkeypatch.setattr(
|
||||
onboard_wizard,
|
||||
"_input_model_with_autocomplete",
|
||||
lambda *a, **kw: "custom-model",
|
||||
)
|
||||
|
||||
assert onboard_wizard._configure_quick_start_provider(config) is True
|
||||
|
||||
assert calls == {"api_base": "https://api.example.test/v1", "api_key": "sk-custom-test"}
|
||||
assert config.providers.custom.api_key == "sk-custom-test"
|
||||
assert config.providers.custom.api_base == "https://api.example.test/v1"
|
||||
assert config.model_presets["primary"].provider == "custom"
|
||||
@@ -1038,6 +1024,21 @@ class TestMainMenuUpdate:
|
||||
assert config.providers.custom.api_base is None
|
||||
assert "primary" not in config.model_presets
|
||||
|
||||
def test_quick_start_requires_model_id_before_setting_defaults(self, monkeypatch):
|
||||
"""Quick Start should not create a preset without an explicit model ID."""
|
||||
config = Config()
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "sk-ds-test")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", lambda *a, **kw: "")
|
||||
|
||||
assert onboard_wizard._configure_quick_start_provider(config) is False
|
||||
|
||||
assert config.providers.deepseek.api_key is None
|
||||
assert config.providers.deepseek.api_base is None
|
||||
assert "primary" not in config.model_presets
|
||||
|
||||
def test_quick_start_summary_calls_out_missing_api_key(self, monkeypatch):
|
||||
"""Quick Start summary should not tell users to run gateway before adding a key."""
|
||||
config = Config()
|
||||
|
||||
Reference in New Issue
Block a user