fix(setup): refresh LLM provider wizard defaults (#3421)

This commit is contained in:
Nan Gao 2026-06-08 02:33:24 +02:00 committed by GitHub
parent 10c1d9f417
commit 3b4c9ff733
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 426 additions and 21 deletions

View File

@ -7,7 +7,8 @@ Run from repo root:
from __future__ import annotations
import yaml
from wizard.providers import LLM_PROVIDERS, SEARCH_PROVIDERS, WEB_FETCH_PROVIDERS
from wizard.providers import LLM_PROVIDERS, SEARCH_PROVIDERS, WEB_FETCH_PROVIDERS, LLMProvider
from wizard.steps import llm as llm_step
from wizard.steps import search as search_step
from wizard.writer import (
build_minimal_config,
@ -21,6 +22,38 @@ class TestProviders:
def test_llm_providers_not_empty(self):
assert len(LLM_PROVIDERS) >= 8
def test_llm_providers_cover_config_example_families(self):
providers = {provider.name: provider for provider in LLM_PROVIDERS}
expected = {
"volcengine",
"openai",
"openai_responses",
"ollama_qwen",
"ollama_gemma",
"anthropic",
"google",
"gemini_openai_gateway",
"mimo",
"deepseek",
"kimi",
"novita",
"minimax",
"minimax_cn",
"openrouter",
"vllm",
"mindie",
"codex",
"claude_code",
}
assert expected.issubset(providers)
assert providers["openai_responses"].extra_config["use_responses_api"] is True
assert providers["gemini_openai_gateway"].use == "deerflow.models.patched_openai:PatchedChatOpenAI"
assert providers["mimo"].use == "deerflow.models.patched_mimo:PatchedChatMiMo"
assert providers["deepseek"].use == "deerflow.models.patched_deepseek:PatchedChatDeepSeek"
assert providers["volcengine"].extra_config["api_base"] == "https://ark.cn-beijing.volces.com/api/v3"
def test_llm_providers_have_required_fields(self):
for p in LLM_PROVIDERS:
assert p.name
@ -236,6 +269,97 @@ class TestBuildMinimalConfig:
model = data["models"][0]
assert "api_key" not in model
def test_responses_api_provider_defaults_are_preserved(self):
provider = next(p for p in LLM_PROVIDERS if p.name == "openai_responses")
content = build_minimal_config(
provider_use=provider.use,
model_name=provider.default_model,
display_name=provider.display_name,
api_key_field=provider.api_key_field,
env_var=provider.env_var,
extra_model_config=provider.extra_config,
)
data = yaml.safe_load(content)
model = data["models"][0]
assert model["use_responses_api"] is True
assert model["output_version"] == "responses/v1"
assert model["supports_vision"] is True
def test_patched_thinking_provider_defaults_are_preserved(self):
provider = next(p for p in LLM_PROVIDERS if p.name == "mimo")
content = build_minimal_config(
provider_use=provider.use,
model_name=provider.default_model,
display_name=provider.display_name,
api_key_field=provider.api_key_field,
env_var=provider.env_var,
extra_model_config=provider.extra_config,
)
data = yaml.safe_load(content)
model = data["models"][0]
assert model["use"] == "deerflow.models.patched_mimo:PatchedChatMiMo"
assert model["base_url"] == "https://api.xiaomimimo.com/v1"
assert model["api_key"] == "$MIMO_API_KEY"
assert model["supports_thinking"] is True
assert model["when_thinking_enabled"]["extra_body"]["thinking"]["type"] == "enabled"
assert model["when_thinking_disabled"]["extra_body"]["thinking"]["type"] == "disabled"
class TestLLMStep:
def test_model_selection_defaults_to_provider_default_model(self, monkeypatch):
provider = LLMProvider(
name="test",
display_name="Test",
description="provider",
use="langchain_openai:ChatOpenAI",
models=["first-model", "default-model"],
default_model="default-model",
env_var="TEST_API_KEY",
package="langchain-openai",
)
prompts: list[tuple[str, int | None]] = []
def fake_choice(prompt, options, default=None):
prompts.append((prompt, default))
return default if default is not None else 0
monkeypatch.setattr(llm_step, "LLM_PROVIDERS", [provider])
monkeypatch.setattr(llm_step, "ask_choice", fake_choice)
monkeypatch.setattr(llm_step, "ask_secret", lambda _prompt: "key")
monkeypatch.setattr(llm_step, "print_header", lambda *_args, **_kwargs: None)
monkeypatch.setattr(llm_step, "print_info", lambda *_args, **_kwargs: None)
monkeypatch.setattr(llm_step, "print_success", lambda *_args, **_kwargs: None)
result = llm_step.run_llm_step()
assert result.model_name == "default-model"
assert prompts == [("Enter choice", None), ("Select model", 1)]
def test_base_url_prompt_is_used_for_custom_gateway(self, monkeypatch):
provider = LLMProvider(
name="gateway",
display_name="Gateway",
description="provider",
use="langchain_openai:ChatOpenAI",
models=["gateway/model"],
default_model="gateway/model",
env_var="GATEWAY_API_KEY",
package="langchain-openai",
base_url_prompt="Gateway URL",
)
monkeypatch.setattr(llm_step, "LLM_PROVIDERS", [provider])
monkeypatch.setattr(llm_step, "ask_choice", lambda *_args, **_kwargs: 0)
monkeypatch.setattr(llm_step, "ask_text", lambda *_args, **_kwargs: "https://gateway.example/v1")
monkeypatch.setattr(llm_step, "ask_secret", lambda _prompt: "key")
monkeypatch.setattr(llm_step, "print_header", lambda *_args, **_kwargs: None)
monkeypatch.setattr(llm_step, "print_info", lambda *_args, **_kwargs: None)
monkeypatch.setattr(llm_step, "print_success", lambda *_args, **_kwargs: None)
result = llm_step.run_llm_step()
assert result.base_url == "https://gateway.example/v1"
# ---------------------------------------------------------------------------
# writer.py — env file helpers

View File

@ -20,6 +20,8 @@ class LLMProvider:
# Extra config fields beyond the common ones (merged into YAML)
extra_config: dict = field(default_factory=dict)
auth_hint: str | None = None
base_url_prompt: str | None = None
model_prompt: str | None = None
@dataclass
@ -44,48 +46,292 @@ class SearchProvider:
extra_config: dict = field(default_factory=dict)
OPENAI_COMPAT_THINKING_CONFIG = {
"supports_thinking": True,
"when_thinking_enabled": {
"extra_body": {
"thinking": {
"type": "enabled",
}
}
},
"when_thinking_disabled": {
"extra_body": {
"thinking": {
"type": "disabled",
}
}
},
}
ANTHROPIC_THINKING_CONFIG = {
"supports_thinking": True,
"when_thinking_enabled": {
"thinking": {
"type": "enabled",
"budget_tokens": 4096,
}
},
"when_thinking_disabled": {
"thinking": {
"type": "disabled",
}
},
}
LLM_PROVIDERS: list[LLMProvider] = [
LLMProvider(
name="volcengine",
display_name="Volcengine Doubao",
description="Doubao Seed with thinking support",
use="deerflow.models.patched_deepseek:PatchedChatDeepSeek",
models=["doubao-seed-1-8-251228"],
default_model="doubao-seed-1-8-251228",
env_var="VOLCENGINE_API_KEY",
package="langchain-deepseek",
extra_config={
"api_base": "https://ark.cn-beijing.volces.com/api/v3",
"timeout": 600.0,
"max_retries": 2,
"supports_vision": True,
"supports_reasoning_effort": True,
**OPENAI_COMPAT_THINKING_CONFIG,
},
),
LLMProvider(
name="openai",
display_name="OpenAI",
description="GPT-4o, GPT-4.1, o3",
description="GPT-5, GPT-4.1, GPT-4o",
use="langchain_openai:ChatOpenAI",
models=["gpt-4o", "gpt-4.1", "o3"],
default_model="gpt-4o",
models=["gpt-5", "gpt-5-mini", "gpt-4.1", "gpt-4o"],
default_model="gpt-5",
env_var="OPENAI_API_KEY",
package="langchain-openai",
extra_config={
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 4096,
"temperature": 0.7,
"supports_vision": True,
},
),
LLMProvider(
name="openai_responses",
display_name="OpenAI Responses API",
description="GPT-5 via /v1/responses",
use="langchain_openai:ChatOpenAI",
models=["gpt-5", "gpt-5-mini"],
default_model="gpt-5",
env_var="OPENAI_API_KEY",
package="langchain-openai",
extra_config={
"request_timeout": 600.0,
"max_retries": 2,
"use_responses_api": True,
"output_version": "responses/v1",
"supports_vision": True,
},
),
LLMProvider(
name="anthropic",
display_name="Anthropic",
description="Claude Opus 4, Sonnet 4",
description="Claude Sonnet 4 with extended thinking",
use="langchain_anthropic:ChatAnthropic",
models=["claude-opus-4-5", "claude-sonnet-4-5"],
default_model="claude-sonnet-4-5",
models=["claude-sonnet-4-20250514", "claude-opus-4-5", "claude-sonnet-4-5"],
default_model="claude-sonnet-4-20250514",
env_var="ANTHROPIC_API_KEY",
package="langchain-anthropic",
extra_config={"max_tokens": 8192},
extra_config={
"default_request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 16000,
"supports_vision": True,
**ANTHROPIC_THINKING_CONFIG,
},
),
LLMProvider(
name="deepseek",
display_name="DeepSeek",
description="V3, R1",
use="langchain_deepseek:ChatDeepSeek",
models=["deepseek-chat", "deepseek-reasoner"],
default_model="deepseek-chat",
description="DeepSeek Reasoner with thinking support",
use="deerflow.models.patched_deepseek:PatchedChatDeepSeek",
models=["deepseek-reasoner", "deepseek-chat"],
default_model="deepseek-reasoner",
env_var="DEEPSEEK_API_KEY",
package="langchain-deepseek",
extra_config={
"timeout": 600.0,
"max_retries": 2,
"max_tokens": 8192,
"supports_vision": False,
**OPENAI_COMPAT_THINKING_CONFIG,
},
),
LLMProvider(
name="google",
display_name="Google Gemini",
description="2.0 Flash, 2.5 Pro",
description="Native Gemini SDK, no thinking support",
use="langchain_google_genai:ChatGoogleGenerativeAI",
models=["gemini-2.0-flash", "gemini-2.5-pro"],
default_model="gemini-2.0-flash",
models=["gemini-2.5-pro", "gemini-2.0-flash"],
default_model="gemini-2.5-pro",
env_var="GEMINI_API_KEY",
package="langchain-google-genai",
api_key_field="gemini_api_key",
extra_config={
"timeout": 600.0,
"max_retries": 2,
"max_tokens": 8192,
"supports_vision": True,
},
),
LLMProvider(
name="gemini_openai_gateway",
display_name="Gemini OpenAI-compatible",
description="Gemini thinking via an OpenAI-compatible gateway",
use="deerflow.models.patched_openai:PatchedChatOpenAI",
models=["google/gemini-2.5-pro-preview"],
default_model="google/gemini-2.5-pro-preview",
env_var="GEMINI_API_KEY",
package="langchain-openai",
extra_config={
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 16384,
"supports_vision": True,
**OPENAI_COMPAT_THINKING_CONFIG,
},
base_url_prompt="Gateway base URL (e.g. https://your-gateway.example/v1)",
),
LLMProvider(
name="ollama_qwen",
display_name="Ollama Qwen3",
description="Native local Ollama provider with thinking support",
use="langchain_ollama:ChatOllama",
models=["qwen3:32b"],
default_model="qwen3:32b",
env_var=None,
package="langchain-ollama",
extra_config={
"base_url": "http://localhost:11434",
"num_predict": 8192,
"temperature": 0.7,
"reasoning": True,
"supports_thinking": True,
"supports_vision": False,
},
auth_hint="No API key is required. Ensure Ollama is running and the model is pulled.",
),
LLMProvider(
name="ollama_gemma",
display_name="Ollama Gemma",
description="Native local Ollama provider with vision support",
use="langchain_ollama:ChatOllama",
models=["gemma4:27b"],
default_model="gemma4:27b",
env_var=None,
package="langchain-ollama",
extra_config={
"base_url": "http://localhost:11434",
"num_predict": 8192,
"temperature": 0.7,
"reasoning": True,
"supports_thinking": True,
"supports_vision": True,
},
auth_hint="No API key is required. Ensure Ollama is running and the model is pulled.",
),
LLMProvider(
name="mimo",
display_name="Xiaomi MiMo",
description="MiMo thinking models with reasoning replay",
use="deerflow.models.patched_mimo:PatchedChatMiMo",
models=["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "mimo-v2-flash"],
default_model="mimo-v2.5-pro",
env_var="MIMO_API_KEY",
package="langchain-openai",
extra_config={
"base_url": "https://api.xiaomimimo.com/v1",
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 8192,
"supports_vision": False,
**OPENAI_COMPAT_THINKING_CONFIG,
},
),
LLMProvider(
name="kimi",
display_name="Moonshot Kimi",
description="Kimi K2.5 with thinking support",
use="deerflow.models.patched_deepseek:PatchedChatDeepSeek",
models=["kimi-k2.5"],
default_model="kimi-k2.5",
env_var="MOONSHOT_API_KEY",
package="langchain-deepseek",
extra_config={
"api_base": "https://api.moonshot.cn/v1",
"timeout": 600.0,
"max_retries": 2,
"max_tokens": 32768,
"supports_vision": True,
**OPENAI_COMPAT_THINKING_CONFIG,
},
),
LLMProvider(
name="novita",
display_name="Novita AI",
description="DeepSeek V3.2 via OpenAI-compatible API",
use="langchain_openai:ChatOpenAI",
models=["deepseek/deepseek-v3.2"],
default_model="deepseek/deepseek-v3.2",
env_var="NOVITA_API_KEY",
package="langchain-openai",
extra_config={
"base_url": "https://api.novita.ai/openai",
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 4096,
"temperature": 0.7,
"supports_vision": True,
**OPENAI_COMPAT_THINKING_CONFIG,
},
),
LLMProvider(
name="minimax",
display_name="MiniMax",
description="International OpenAI-compatible endpoint",
use="langchain_openai:ChatOpenAI",
models=["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"],
default_model="MiniMax-M3",
env_var="MINIMAX_API_KEY",
package="langchain-openai",
extra_config={
"base_url": "https://api.minimax.io/v1",
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 4096,
"temperature": 1.0,
"supports_vision": True,
"supports_thinking": True,
},
),
LLMProvider(
name="minimax_cn",
display_name="MiniMax CN",
description="China OpenAI-compatible endpoint",
use="langchain_openai:ChatOpenAI",
models=["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"],
default_model="MiniMax-M3",
env_var="MINIMAX_API_KEY",
package="langchain-openai",
extra_config={
"base_url": "https://api.minimaxi.com/v1",
"request_timeout": 600.0,
"max_retries": 2,
"max_tokens": 4096,
"temperature": 1.0,
"supports_vision": True,
"supports_thinking": True,
},
),
LLMProvider(
name="openrouter",
@ -127,6 +373,35 @@ LLM_PROVIDERS: list[LLMProvider] = [
}
}
},
"when_thinking_disabled": {
"extra_body": {
"chat_template_kwargs": {
"enable_thinking": False,
}
}
},
},
),
LLMProvider(
name="mindie",
display_name="MindIE",
description="Qwen3-Coder on MindIE Engine",
use="deerflow.models.mindie_provider:MindIEChatModel",
models=["Qwen3-Coder-480B-A35B-Instruct-Client"],
default_model="Qwen3-Coder-480B-A35B-Instruct-Client",
env_var="OPENAI_API_KEY",
package=None,
extra_config={
"base_url": "http://localhost:8989/v1",
"temperature": 0,
"max_retries": 1,
"supports_thinking": False,
"supports_vision": False,
"supports_reasoning_effort": False,
"read_timeout": 900.0,
"connect_timeout": 30.0,
"write_timeout": 60.0,
"pool_timeout": 30.0,
},
),
LLMProvider(
@ -163,6 +438,8 @@ LLM_PROVIDERS: list[LLMProvider] = [
default_model="gpt-4o",
env_var="OPENAI_API_KEY",
package="langchain-openai",
base_url_prompt="Base URL (e.g. https://api.openai.com/v1)",
model_prompt="Model name",
),
]

View File

@ -32,10 +32,11 @@ def run_llm_step(step_label: str = "Step 1/3") -> LLMStepResult:
print()
# Model selection (show list, default to first)
# Model selection (show list, default to provider preference)
if len(provider.models) > 1:
print_info(f"Available models for {provider.display_name}:")
model_idx = ask_choice("Select model", provider.models, default=0)
default_model_idx = provider.models.index(provider.default_model)
model_idx = ask_choice("Select model", provider.models, default=default_model_idx)
model_name = provider.models[model_idx]
else:
model_name = provider.models[0]
@ -44,11 +45,14 @@ def run_llm_step(step_label: str = "Step 1/3") -> LLMStepResult:
base_url: str | None = None
if provider.name in {"openrouter", "vllm"}:
base_url = provider.extra_config.get("base_url")
if provider.name == "other":
if provider.base_url_prompt:
print_header(f"{step_label} · Connection details")
base_url = ask_text("Base URL (e.g. https://api.openai.com/v1)", required=True)
model_name = ask_text("Model name", default=provider.default_model)
elif provider.auth_hint:
base_url = ask_text(provider.base_url_prompt, default=base_url or "", required=True)
if provider.model_prompt:
model_name = ask_text(provider.model_prompt, default=model_name)
if provider.auth_hint:
print_header(f"{step_label} · Authentication")
print_info(provider.auth_hint)
api_key = None