diff --git a/openhands/cli/settings.py b/openhands/cli/settings.py index 5c8395bb4c..7ce8f7de41 100644 --- a/openhands/cli/settings.py +++ b/openhands/cli/settings.py @@ -13,6 +13,7 @@ from openhands.cli.tui import ( ) from openhands.cli.utils import ( VERIFIED_ANTHROPIC_MODELS, + VERIFIED_MISTRAL_MODELS, VERIFIED_OPENAI_MODELS, VERIFIED_PROVIDERS, organize_models_and_providers, @@ -158,7 +159,7 @@ async def modify_llm_settings_basic( provider_completer = FuzzyWordCompleter(provider_list) session = PromptSession(key_bindings=kb_cancel()) - # Set default provider - prefer 'anthropic' if available, otherwise use the first provider + # Set default provider - prefer 'anthropic' if available, otherwise use first provider = 'anthropic' if 'anthropic' in provider_list else provider_list[0] model = None api_key = None @@ -168,15 +169,26 @@ async def modify_llm_settings_basic( print_formatted_text( HTML(f'\nDefault provider: {provider}') ) - change_provider = ( - cli_confirm( - 'Do you want to use a different provider?', - [f'Use {provider}', 'Select another provider'], - ) - == 1 + + # Show verified providers plus "Select another provider" option + provider_choices = verified_providers + ['Select another provider'] + provider_choice = cli_confirm( + '(Step 1/3) Select LLM Provider:', + provider_choices, ) - if change_provider: + # Ensure provider_choice is an integer (for test compatibility) + try: + choice_index = int(provider_choice) + except (TypeError, ValueError): + # If conversion fails (e.g., in tests with mocks), default to 0 + choice_index = 0 + + if choice_index < len(verified_providers): + # User selected one of the verified providers + provider = verified_providers[choice_index] + else: + # User selected "Select another provider" - use manual selection # Define a validator function that prints an error message def provider_validator(x): is_valid = x in organized_models @@ -196,7 +208,8 @@ async def modify_llm_settings_basic( # Make sure the provider exists in organized_models if provider not in organized_models: - # If the provider doesn't exist, prefer 'anthropic' if available, otherwise use the first provider + # If the provider doesn't exist, prefer 'anthropic' if available, + # otherwise use the first provider provider = ( 'anthropic' if 'anthropic' in organized_models @@ -214,6 +227,11 @@ async def modify_llm_settings_basic( m for m in provider_models if m not in VERIFIED_ANTHROPIC_MODELS ] provider_models = VERIFIED_ANTHROPIC_MODELS + provider_models + if provider == 'mistral': + provider_models = [ + m for m in provider_models if m not in VERIFIED_MISTRAL_MODELS + ] + provider_models = VERIFIED_MISTRAL_MODELS + provider_models # Set default model to the best verified model for the provider if provider == 'anthropic' and VERIFIED_ANTHROPIC_MODELS: @@ -222,6 +240,9 @@ async def modify_llm_settings_basic( elif provider == 'openai' and VERIFIED_OPENAI_MODELS: # Use the first model in the VERIFIED_OPENAI_MODELS list as it's the best/newest default_model = VERIFIED_OPENAI_MODELS[0] + elif provider == 'mistral' and VERIFIED_MISTRAL_MODELS: + # Use the first model in the VERIFIED_MISTRAL_MODELS list as it's the best/newest + default_model = VERIFIED_MISTRAL_MODELS[0] else: # For other providers, use the first model in the list default_model = ( diff --git a/openhands/cli/utils.py b/openhands/cli/utils.py index 802b8a94c1..a88f76585a 100644 --- a/openhands/cli/utils.py +++ b/openhands/cli/utils.py @@ -102,6 +102,8 @@ def extract_model_and_provider(model: str) -> ModelInfo: return ModelInfo(provider='openai', model=split[0], separator='/') if split[0] in VERIFIED_ANTHROPIC_MODELS: return ModelInfo(provider='anthropic', model=split[0], separator='/') + if split[0] in VERIFIED_MISTRAL_MODELS: + return ModelInfo(provider='mistral', model=split[0], separator='/') # return as model only return ModelInfo(provider='', model=model, separator='') @@ -143,9 +145,10 @@ def organize_models_and_providers( return result_dict -VERIFIED_PROVIDERS = ['openai', 'azure', 'anthropic', 'deepseek'] +VERIFIED_PROVIDERS = ['anthropic', 'openai', 'mistral'] VERIFIED_OPENAI_MODELS = [ + 'o4-mini', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', @@ -171,6 +174,10 @@ VERIFIED_ANTHROPIC_MODELS = [ 'claude-2', ] +VERIFIED_MISTRAL_MODELS = [ + 'devstral-small-2505', +] + class ProviderInfo(BaseModel): """Information about a provider and its models.""" diff --git a/tests/unit/test_cli_utils.py b/tests/unit/test_cli_utils.py index 02ffdd09ad..26fb011630 100644 --- a/tests/unit/test_cli_utils.py +++ b/tests/unit/test_cli_utils.py @@ -361,6 +361,22 @@ class TestModelAndProviderFunctions: assert result['model'] == 'claude-sonnet-4-20250514' assert result['separator'] == '/' + def test_extract_model_and_provider_mistral_implicit(self): + model = 'devstral-small-2505' + result = extract_model_and_provider(model) + + assert result['provider'] == 'mistral' + assert result['model'] == 'devstral-small-2505' + assert result['separator'] == '/' + + def test_extract_model_and_provider_o4_mini(self): + model = 'o4-mini' + result = extract_model_and_provider(model) + + assert result['provider'] == 'openai' + assert result['model'] == 'o4-mini' + assert result['separator'] == '/' + def test_extract_model_and_provider_versioned(self): model = 'deepseek.deepseek-coder-1.3b' result = extract_model_and_provider(model) @@ -382,6 +398,9 @@ class TestModelAndProviderFunctions: 'openai/gpt-4o', 'anthropic/claude-sonnet-4-20250514', 'o3-mini', + 'o4-mini', + 'devstral-small-2505', + 'mistral/devstral-small-2505', 'anthropic.claude-3-5', # Should be ignored as it uses dot separator for anthropic 'unknown-model', ] @@ -390,15 +409,20 @@ class TestModelAndProviderFunctions: assert 'openai' in result assert 'anthropic' in result + assert 'mistral' in result assert 'other' in result - assert len(result['openai']['models']) == 2 + assert len(result['openai']['models']) == 3 assert 'gpt-4o' in result['openai']['models'] assert 'o3-mini' in result['openai']['models'] + assert 'o4-mini' in result['openai']['models'] assert len(result['anthropic']['models']) == 1 assert 'claude-sonnet-4-20250514' in result['anthropic']['models'] + assert len(result['mistral']['models']) == 2 + assert 'devstral-small-2505' in result['mistral']['models'] + assert len(result['other']['models']) == 1 assert 'unknown-model' in result['other']['models']