Add o4-mini model and Mistral provider support to OpenHands CLI (#9217)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-06-19 14:47:27 -04:00 committed by GitHub
parent 8c5995a5d8
commit 516f9fa635
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 11 deletions

View File

@ -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'\n<grey>Default provider: </grey><green>{provider}</green>')
)
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 = (

View File

@ -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."""

View File

@ -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']