fix: make quick start provider neutral
Maintainer edit: restart the Quick Start flow around an API-key-first path without recommending OpenRouter or DeepSeek. Detect unique key prefixes locally, fall back to a user-provided base URL, and only fetch models from that approved URL.
This commit is contained in:
@@ -304,15 +304,16 @@ Skip this step if you already configured provider and model settings in the wiza
|
||||
|
||||
`nanobot onboard --defaults` creates `~/.nanobot/config.json` and `~/.nanobot/workspace/`. Configure these **two parts** in the config file. Add or merge the following blocks into the existing file instead of replacing the whole file.
|
||||
|
||||
The example below uses [DeepSeek](https://platform.deepseek.com/api_keys) only so the JSON has concrete names and the compact path does not depend on OpenRouter. Provider examples are recipes, not rankings or endorsements. If you use another provider, replace the provider config key, API key, preset provider name, and model ID together.
|
||||
The example below uses a generic OpenAI-compatible `custom` provider so the compact path does not recommend one hosted service. Provider examples are recipes, not rankings or endorsements. For copyable provider-specific setup, see [Provider Cookbook](./docs/provider-cookbook.md).
|
||||
|
||||
*Set your API key*:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"apiKey": "your-deepseek-api-key"
|
||||
"custom": {
|
||||
"apiKey": "your-api-key",
|
||||
"apiBase": "https://api.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,8 +326,8 @@ The example below uses [DeepSeek](https://platform.deepseek.com/api_keys) only s
|
||||
"modelPresets": {
|
||||
"primary": {
|
||||
"label": "Primary",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"provider": "custom",
|
||||
"model": "model-id-from-your-provider",
|
||||
"maxTokens": 8192,
|
||||
"contextWindowTokens": 65536,
|
||||
"temperature": 0.1
|
||||
|
||||
+11
-9
@@ -9,7 +9,7 @@ If you have never used a terminal or edited a config file before, use [`start-wi
|
||||
You need:
|
||||
|
||||
- Python 3.11 or newer.
|
||||
- One LLM provider, company endpoint, subscription endpoint, or local model server you can call. The examples below use DeepSeek only so the snippets are concrete and the compact path does not depend on OpenRouter; any supported provider works when the key, provider name, and model ID match.
|
||||
- One LLM provider, company endpoint, subscription endpoint, or local model server you can call. The examples below use a generic OpenAI-compatible `custom` provider so the compact path does not recommend one hosted service; any supported provider works when the key, provider name, and model ID match.
|
||||
- Git only if you install from source.
|
||||
- Node.js or Bun only if you are developing the WebUI itself.
|
||||
|
||||
@@ -128,8 +128,9 @@ Open `~/.nanobot/config.json`. Add or merge these blocks into the file created b
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"apiKey": "your-deepseek-api-key"
|
||||
"custom": {
|
||||
"apiKey": "your-api-key",
|
||||
"apiBase": "https://api.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,8 +143,8 @@ Open `~/.nanobot/config.json`. Add or merge these blocks into the file created b
|
||||
"modelPresets": {
|
||||
"primary": {
|
||||
"label": "Primary",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"provider": "custom",
|
||||
"model": "model-id-from-your-provider",
|
||||
"maxTokens": 8192,
|
||||
"contextWindowTokens": 65536,
|
||||
"temperature": 0.1
|
||||
@@ -161,7 +162,7 @@ The provider and model inside a preset must match. The snippet above is only an
|
||||
|
||||
| Replace | Where |
|
||||
|---|---|
|
||||
| Provider config key, such as `deepseek` | `providers.<provider>` |
|
||||
| Provider config key, such as `custom` | `providers.<provider>` |
|
||||
| API key or environment variable | `providers.<provider>.apiKey` |
|
||||
| Preset provider name | `modelPresets.primary.provider` |
|
||||
| Model ID | `modelPresets.primary.model` |
|
||||
@@ -207,8 +208,9 @@ If you prefer not to store secrets in `config.json`, reference an environment va
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"apiKey": "${DEEPSEEK_API_KEY}"
|
||||
"custom": {
|
||||
"apiKey": "${PROVIDER_API_KEY}",
|
||||
"apiBase": "https://api.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +262,7 @@ Example prompt:
|
||||
|
||||
```text
|
||||
Read docs/quick-start.md, docs/providers.md, and docs/configuration.md in this checkout.
|
||||
Then update ~/.nanobot/config.json to add a DeepSeek model preset named "primary".
|
||||
Then update ~/.nanobot/config.json to add a model preset named "primary" for my provider.
|
||||
Tell me exactly what changed and whether I need to run /restart.
|
||||
```
|
||||
|
||||
|
||||
@@ -59,15 +59,14 @@ If `python3` works but `python` does not, replace `python` with `python3` in the
|
||||
|
||||
## 3. Get a Provider API Key
|
||||
|
||||
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use DeepSeek because it is the recommended beginner path in the wizard and avoids making OpenRouter a first-run dependency; it is not a ranking or endorsement.
|
||||
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. If the provider has an OpenAI-compatible base URL in its docs, keep that nearby too.
|
||||
|
||||
For the example path:
|
||||
For the setup path:
|
||||
|
||||
1. Open [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys).
|
||||
1. Open your provider's API key page.
|
||||
2. Create or copy an API key.
|
||||
3. Keep the key private.
|
||||
|
||||
Keep the key nearby because the setup wizard will ask you to paste it.
|
||||
4. Keep the provider's base URL nearby if the provider docs show one.
|
||||
|
||||
## 4. Install nanobot
|
||||
|
||||
@@ -157,7 +156,7 @@ You will see a menu like this:
|
||||
|
||||
```text
|
||||
> What would you like to do?
|
||||
[Q] Quick Start (API key only)
|
||||
[Q] Quick Start (API key first)
|
||||
[A] Advanced Settings
|
||||
[X] Exit
|
||||
```
|
||||
@@ -167,18 +166,18 @@ Move through the wizard like this:
|
||||
| When you see | Do this |
|
||||
|---|---|
|
||||
| A menu | Use the arrow keys to highlight an option, then press `Enter`. |
|
||||
| The DeepSeek API key field | Paste the key, then press `Enter`. |
|
||||
| The API key field | Paste the key, then press `Enter`. |
|
||||
| A base URL field | Paste the provider base URL from its docs, 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 (API key only)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a different provider, chat app, or tool setup.
|
||||
For the first setup, choose `[Q] Quick Start (API key first)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a different provider, chat app, or tool setup.
|
||||
|
||||
If you are following the DeepSeek example:
|
||||
1. Choose `[Q] Quick Start (API key first)`.
|
||||
2. Paste your API key.
|
||||
3. If nanobot cannot recognize the key, paste the provider base URL from that provider's docs.
|
||||
4. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||
|
||||
1. Choose `[Q] Quick Start (API key only)`.
|
||||
2. Paste your DeepSeek API key.
|
||||
3. 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 model, endpoint, or chat channel for the first run.
|
||||
The recommended path enables the local WebUI and default AI settings. You do not need to choose a chat channel for the first run.
|
||||
|
||||
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`.
|
||||
|
||||
@@ -211,8 +210,9 @@ Merge them into one object:
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"apiKey": "your-deepseek-api-key"
|
||||
"custom": {
|
||||
"apiKey": "your-api-key",
|
||||
"apiBase": "https://api.example.com/v1"
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
@@ -256,15 +256,16 @@ If this is a brand-new install and you have not configured anything else yet, re
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"deepseek": {
|
||||
"apiKey": "your-deepseek-api-key"
|
||||
"custom": {
|
||||
"apiKey": "your-api-key",
|
||||
"apiBase": "https://api.example.com/v1"
|
||||
}
|
||||
},
|
||||
"modelPresets": {
|
||||
"primary": {
|
||||
"label": "Primary",
|
||||
"provider": "deepseek",
|
||||
"model": "deepseek-v4-flash",
|
||||
"provider": "custom",
|
||||
"model": "model-id-from-your-provider",
|
||||
"maxTokens": 4096,
|
||||
"contextWindowTokens": 65536,
|
||||
"temperature": 0.1
|
||||
@@ -283,9 +284,9 @@ If this is a brand-new install and you have not configured anything else yet, re
|
||||
}
|
||||
```
|
||||
|
||||
Replace `your-deepseek-api-key` with your real DeepSeek key.
|
||||
Replace `your-api-key`, `https://api.example.com/v1`, and `model-id-from-your-provider` with values from your provider.
|
||||
|
||||
If you use another provider, replace `deepseek`, `your-deepseek-api-key`, and the `model` value with that provider's values. If the provider needs `apiBase`, add it under that provider's config block.
|
||||
For copyable provider-specific examples, use [`provider-cookbook.md`](./provider-cookbook.md).
|
||||
|
||||
Save the file.
|
||||
|
||||
|
||||
+115
-24
@@ -54,8 +54,9 @@ _BACK_PRESSED = object() # Sentinel value for back navigation
|
||||
# offer existing presets as choices (e.g. AgentDefaults.model_preset).
|
||||
_MODEL_PRESET_CACHE: set[str] = set()
|
||||
|
||||
_QUICK_START_RECOMMENDED_PROVIDER = "deepseek"
|
||||
_QUICK_START_RECOMMENDED_MODEL = "deepseek-v4-flash"
|
||||
_QUICK_START_DEFAULT_MODELS = {
|
||||
"deepseek": "deepseek-v4-flash",
|
||||
}
|
||||
|
||||
_QUICK_START_STEPS = ("API key", "WebUI", "Review")
|
||||
|
||||
@@ -388,7 +389,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 only needs a DeepSeek API key.[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start starts with one API key.[/]")
|
||||
body.add_row(
|
||||
f"[{_UI_MUTED}]Use Advanced later for other providers or chat apps.[/]"
|
||||
)
|
||||
@@ -1410,36 +1411,126 @@ def _show_quick_start_progress(active_step: int) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def _configure_recommended_provider(config: Config) -> bool:
|
||||
"""Configure the beginner path provider with one API-key prompt."""
|
||||
def _detect_quick_start_provider_from_key(api_key: str) -> str | None:
|
||||
"""Return a provider when the API key prefix identifies exactly one provider."""
|
||||
from nanobot.providers.registry import PROVIDERS
|
||||
|
||||
matches = [
|
||||
spec.name
|
||||
for spec in PROVIDERS
|
||||
if spec.detect_by_key_prefix
|
||||
and api_key.startswith(spec.detect_by_key_prefix)
|
||||
and not spec.is_oauth
|
||||
and not spec.is_transcription_only
|
||||
]
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def _detect_quick_start_provider_from_base(api_base: str) -> str | None:
|
||||
"""Return a provider when the user-provided base URL matches registry metadata."""
|
||||
from nanobot.providers.registry import PROVIDERS
|
||||
|
||||
normalized = api_base.rstrip("/").lower()
|
||||
for spec in PROVIDERS:
|
||||
if spec.is_oauth or spec.is_transcription_only:
|
||||
continue
|
||||
default_base = spec.default_api_base.rstrip("/").lower()
|
||||
if default_base and (normalized == default_base or normalized.startswith(default_base + "/")):
|
||||
return spec.name
|
||||
keyword = spec.detect_by_base_keyword.lower()
|
||||
if keyword and keyword in normalized:
|
||||
return spec.name
|
||||
return None
|
||||
|
||||
|
||||
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 one API key plus base URL fallback."""
|
||||
_show_quick_start_progress(1)
|
||||
provider_name = _QUICK_START_RECOMMENDED_PROVIDER
|
||||
|
||||
api_key = _input_text("API key", "", "str")
|
||||
if api_key is None:
|
||||
return False
|
||||
api_key = api_key.strip()
|
||||
if not api_key:
|
||||
console.print("[yellow]! API key is required for Quick Start[/yellow]")
|
||||
return False
|
||||
|
||||
provider_name = _detect_quick_start_provider_from_key(api_key)
|
||||
provider_info = _get_provider_info()
|
||||
api_base = ""
|
||||
if provider_name:
|
||||
_display, _is_gateway, _is_local, api_base = provider_info.get(
|
||||
provider_name, (provider_name, False, False, "")
|
||||
)
|
||||
else:
|
||||
base_answer = _input_text(
|
||||
"Provider base URL (only this URL will be tested)",
|
||||
"",
|
||||
"str",
|
||||
)
|
||||
if base_answer is None:
|
||||
return False
|
||||
api_base = base_answer.strip().rstrip("/")
|
||||
if not api_base:
|
||||
console.print("[yellow]! Provider base URL is required when the key is not recognized[/yellow]")
|
||||
return False
|
||||
provider_name = _detect_quick_start_provider_from_base(api_base) or "custom"
|
||||
|
||||
provider_config = getattr(config.providers, provider_name, None)
|
||||
if provider_config is None:
|
||||
console.print(f"[red]Unknown provider: {provider_name}[/red]")
|
||||
return False
|
||||
|
||||
_display, _is_gateway, _is_local, default_api_base = _get_provider_info().get(
|
||||
provider_name, (provider_name, False, False, "")
|
||||
)
|
||||
provider_config.api_key = api_key
|
||||
if api_base and not provider_config.api_base:
|
||||
provider_config.api_base = api_base
|
||||
|
||||
api_key = _input_with_existing(
|
||||
"DeepSeek API key (get one at https://platform.deepseek.com/api_keys)",
|
||||
provider_config.api_key,
|
||||
"str",
|
||||
)
|
||||
if api_key is not None:
|
||||
provider_config.api_key = api_key.strip() or None
|
||||
if not provider_config.api_key:
|
||||
console.print("[yellow]! DeepSeek API key is required for Quick Start[/yellow]")
|
||||
model = _QUICK_START_DEFAULT_MODELS.get(provider_name)
|
||||
if not model and provider_config.api_base:
|
||||
model = _fetch_first_quick_start_model(provider_config.api_base, api_key)
|
||||
if not model:
|
||||
model = _input_model_with_autocomplete("Model ID", "", provider_name)
|
||||
if not model or not model.strip():
|
||||
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
|
||||
return False
|
||||
if default_api_base and not provider_config.api_base:
|
||||
provider_config.api_base = default_api_base
|
||||
|
||||
_set_primary_quick_start_preset(
|
||||
config,
|
||||
provider_name,
|
||||
_QUICK_START_RECOMMENDED_MODEL,
|
||||
model.strip(),
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -1495,7 +1586,7 @@ def _configure_quick_start(config: Config) -> bool:
|
||||
"Quick Start",
|
||||
"Paste one API key. nanobot will use recommended local WebUI defaults.",
|
||||
)
|
||||
if not _configure_recommended_provider(config):
|
||||
if not _configure_quick_start_provider(config):
|
||||
_pause()
|
||||
return False
|
||||
if not _enable_quick_start_websocket_defaults(config):
|
||||
@@ -1540,7 +1631,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 (API key only)",
|
||||
"[Q] Quick Start (API key first)",
|
||||
"[A] Advanced Settings",
|
||||
]
|
||||
if has_unsaved_changes:
|
||||
@@ -1645,7 +1736,7 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
return OnboardResult(config=original_config, should_save=False)
|
||||
continue
|
||||
|
||||
if answer == "[Q] Quick Start (API key only)":
|
||||
if answer == "[Q] Quick Start (API key first)":
|
||||
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 (API key only)",
|
||||
"[Q] Quick Start (API key first)",
|
||||
"[A] Advanced Settings",
|
||||
"[X] Exit",
|
||||
]
|
||||
@@ -880,7 +880,7 @@ class TestMainMenuUpdate:
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[Q] Quick Start (API key first)",
|
||||
])
|
||||
|
||||
class FakePrompt:
|
||||
@@ -906,21 +906,22 @@ class TestMainMenuUpdate:
|
||||
assert result.should_save is True
|
||||
assert result.config.agents.defaults.bot_name == "quickbot"
|
||||
|
||||
def test_quick_start_recommended_webui_skips_advanced_prompts(self, monkeypatch):
|
||||
"""The beginner path should only ask for the API key and use safe defaults."""
|
||||
def test_quick_start_base_url_fallback_skips_advanced_prompts(self, monkeypatch):
|
||||
"""The beginner path should ask for a base URL only when the key is not recognized."""
|
||||
config = Config()
|
||||
text_answers = iter(["sk-ds-test", "https://api.deepseek.com"])
|
||||
|
||||
def fail_model_input(*_args, **_kwargs):
|
||||
raise AssertionError("recommended Quick Start should not ask for a model ID")
|
||||
raise AssertionError("Quick Start should not ask for a model ID when defaults are known")
|
||||
|
||||
def fail_websocket_config(*_args, **_kwargs):
|
||||
raise AssertionError("recommended Quick Start should not open WebSocket settings")
|
||||
raise AssertionError("Quick Start should not open WebSocket settings")
|
||||
|
||||
pause_messages: list[str] = []
|
||||
|
||||
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-ds-test")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: next(text_answers))
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
|
||||
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
||||
@@ -933,29 +934,75 @@ class TestMainMenuUpdate:
|
||||
assert config.providers.deepseek.api_base == "https://api.deepseek.com"
|
||||
assert config.agents.defaults.model_preset == "primary"
|
||||
assert config.model_presets["primary"].provider == "deepseek"
|
||||
assert config.model_presets["primary"].model == onboard_wizard._QUICK_START_RECOMMENDED_MODEL
|
||||
assert config.model_presets["primary"].model == onboard_wizard._QUICK_START_DEFAULT_MODELS["deepseek"]
|
||||
websocket = getattr(config.channels, "websocket")
|
||||
assert websocket["enabled"] is True
|
||||
assert websocket["websocketRequiresToken"] is True
|
||||
|
||||
def test_quick_start_detects_provider_from_key_prefix(self, monkeypatch):
|
||||
"""Unique key prefixes should identify the provider without asking for base URL."""
|
||||
config = Config()
|
||||
prompts: list[str] = []
|
||||
|
||||
def fake_input_text(prompt, *_args, **_kwargs):
|
||||
prompts.append(prompt)
|
||||
return "sk-or-test"
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", fake_input_text)
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", lambda *a, **kw: "openai/gpt-4o-mini")
|
||||
|
||||
assert onboard_wizard._configure_quick_start_provider(config) is True
|
||||
|
||||
assert prompts == ["API key"]
|
||||
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_custom_base_url_fetches_first_model(self, monkeypatch):
|
||||
"""Unknown providers should use only the user-provided base URL to fetch models."""
|
||||
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(onboard_wizard, "_input_text", lambda *a, **kw: next(text_answers))
|
||||
monkeypatch.setattr(onboard_wizard, "_fetch_first_quick_start_model", fake_fetch)
|
||||
|
||||
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"
|
||||
assert config.model_presets["primary"].model == "custom-model"
|
||||
|
||||
def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch):
|
||||
"""Quick Start should not create a ready-looking config without an API key."""
|
||||
config = Config()
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "")
|
||||
|
||||
assert onboard_wizard._configure_recommended_provider(config) is False
|
||||
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 config.providers.custom.api_key is None
|
||||
assert config.providers.custom.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()
|
||||
config.model_presets["primary"] = ModelPresetConfig(
|
||||
model=onboard_wizard._QUICK_START_RECOMMENDED_MODEL,
|
||||
model=onboard_wizard._QUICK_START_DEFAULT_MODELS["deepseek"],
|
||||
provider="deepseek",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user