diff --git a/cli/main.py b/cli/main.py index ecfc63d59..478b82fde 100644 --- a/cli/main.py +++ b/cli/main.py @@ -559,6 +559,14 @@ def get_user_selections(): ) selected_llm_provider, backend_url = select_llm_provider() + # Providers with regional endpoints prompt for the region as a secondary + # step so the main dropdown stays clean (mainland China and international + # accounts cannot share API keys). + if selected_llm_provider == "qwen": + selected_llm_provider, backend_url = ask_qwen_region() + elif selected_llm_provider == "minimax": + selected_llm_provider, backend_url = ask_minimax_region() + # Step 7: Thinking agents console.print( create_question_box( diff --git a/cli/utils.py b/cli/utils.py index 1fda29203..1245eea78 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -237,10 +237,9 @@ def select_llm_provider() -> tuple[str, str | None]: ("Anthropic", "anthropic", "https://api.anthropic.com/"), ("xAI", "xai", "https://api.x.ai/v1"), ("DeepSeek", "deepseek", "https://api.deepseek.com"), - ("Qwen", "qwen", "https://dashscope.aliyuncs.com/compatible-mode/v1"), + ("Qwen", "qwen", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"), ("GLM", "glm", "https://open.bigmodel.cn/api/paas/v4/"), ("MiniMax", "minimax", "https://api.minimax.io/v1"), - ("MiniMax CN", "minimax-cn", "https://api.minimaxi.com/v1"), ("OpenRouter", "openrouter", "https://openrouter.ai/api/v1"), ("Azure OpenAI", "azure", None), ("Ollama", "ollama", "http://localhost:11434/v1"), @@ -330,6 +329,60 @@ def ask_gemini_thinking_config() -> str | None: ).ask() +def ask_qwen_region() -> tuple[str, str]: + """Ask which Qwen region (international vs China) to use. + + Alibaba DashScope exposes two endpoints with separate accounts — + a key from one region does NOT authenticate against the other + (fixes #758). Returns (provider_key, backend_url). + """ + return questionary.select( + "Select Qwen region:", + choices=[ + questionary.Choice( + "International — dashscope-intl.aliyuncs.com (uses DASHSCOPE_API_KEY)", + value=("qwen", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"), + ), + questionary.Choice( + "China — dashscope.aliyuncs.com (uses DASHSCOPE_CN_API_KEY)", + value=("qwen-cn", "https://dashscope.aliyuncs.com/compatible-mode/v1"), + ), + ], + style=questionary.Style([ + ("selected", "fg:cyan noinherit"), + ("highlighted", "fg:cyan noinherit"), + ("pointer", "fg:cyan noinherit"), + ]), + ).ask() + + +def ask_minimax_region() -> tuple[str, str]: + """Ask which MiniMax region (global vs China) to use. + + MiniMax exposes two endpoints with separate accounts — a key from + one region does NOT authenticate against the other. Returns + (provider_key, backend_url). + """ + return questionary.select( + "Select MiniMax region:", + choices=[ + questionary.Choice( + "Global — api.minimax.io (uses MINIMAX_API_KEY)", + value=("minimax", "https://api.minimax.io/v1"), + ), + questionary.Choice( + "China — api.minimaxi.com (uses MINIMAX_CN_API_KEY)", + value=("minimax-cn", "https://api.minimaxi.com/v1"), + ), + ], + style=questionary.Style([ + ("selected", "fg:cyan noinherit"), + ("highlighted", "fg:cyan noinherit"), + ("pointer", "fg:cyan noinherit"), + ]), + ).ask() + + def ask_output_language() -> str: """Ask for report output language.""" choice = questionary.select( diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index aeb04b1f3..6a9ca999e 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -8,6 +8,31 @@ ModelOption = Tuple[str, str] ProviderModeOptions = Dict[str, Dict[str, List[ModelOption]]] +# Shared model list for Qwen's global (dashscope-intl) and CN (dashscope) endpoints. +# Source: modelstudio.console.alibabacloud.com (Featured Models — Flagship + Cost-optimized). +# +# Only versioned IDs are exposed in the dropdown. The version-less aliases +# (qwen-plus, qwen-flash) are documented by Alibaba as auto-upgrading +# pointers ("backbone, latest, and snapshot ... have been upgraded to the +# Qwen3 series"), which means their behavior shifts when Alibaba rotates +# the backing model. Users who want a specific generation pick it +# explicitly; users who really want auto-latest can enter the alias via +# "Custom model ID". +_QWEN_MODELS: Dict[str, List[ModelOption]] = { + "quick": [ + ("Qwen 3.6 Flash - Latest fast, agentic coding + vision-language", "qwen3.6-flash"), + ("Qwen 3.5 Flash - Previous-gen fast", "qwen3.5-flash"), + ("Custom model ID", "custom"), + ], + "deep": [ + ("Qwen 3.6 Plus - Flagship vision-language, agentic coding SOTA", "qwen3.6-plus"), + ("Qwen 3.5 Plus - Previous-gen flagship", "qwen3.5-plus"), + ("Qwen 3 Max - Specialized for agent programming + tool use", "qwen3-max"), + ("Custom model ID", "custom"), + ], +} + + # Shared model list for MiniMax's global and CN endpoints (same IDs). # Full official lineup per platform.minimax.io/docs/api-reference/text-openai-api. # All M2.x models share a 204,800-token context window. @@ -97,19 +122,10 @@ MODEL_OPTIONS: ProviderModeOptions = { ("Custom model ID", "custom"), ], }, - "qwen": { - "quick": [ - ("Qwen 3.5 Flash", "qwen3.5-flash"), - ("Qwen Plus", "qwen-plus"), - ("Custom model ID", "custom"), - ], - "deep": [ - ("Qwen 3.6 Plus", "qwen3.6-plus"), - ("Qwen 3.5 Plus", "qwen3.5-plus"), - ("Qwen 3 Max", "qwen3-max"), - ("Custom model ID", "custom"), - ], - }, + # Qwen: same model IDs across global (dashscope-intl) and China + # (dashscope) endpoints, so the two provider keys share one model list. + "qwen": _QWEN_MODELS, + "qwen-cn": _QWEN_MODELS, "glm": { "quick": [ ("GLM-4.7", "glm-4.7"),