diff --git a/openhands_cli/tui/modals/settings/choices.py b/openhands_cli/tui/modals/settings/choices.py index 231924eb..3756de9a 100644 --- a/openhands_cli/tui/modals/settings/choices.py +++ b/openhands_cli/tui/modals/settings/choices.py @@ -1,20 +1,49 @@ +import litellm + from openhands.sdk.llm import UNVERIFIED_MODELS_EXCLUDING_BEDROCK, VERIFIED_MODELS +# Get set of valid litellm provider names for filtering +# See: https://docs.litellm.ai/docs/providers +_VALID_LITELLM_PROVIDERS: set[str] = { + str(getattr(p, "value", p)) for p in litellm.provider_list +} + + def get_provider_options() -> list[tuple[str, str]]: - """Get list of available LLM providers.""" - providers = list(VERIFIED_MODELS.keys()) + list( - UNVERIFIED_MODELS_EXCLUDING_BEDROCK.keys() - ) - return [(provider, provider) for provider in providers] + """Get list of available LLM providers. + + Includes: + - All VERIFIED_MODELS providers (openhands, openai, anthropic, mistral) + even if not in litellm.provider_list (e.g. 'openhands' is custom) + - UNVERIFIED providers that are known to litellm (filters out invalid + "providers" like 'meta-llama', 'Qwen' which are vendor names) + + Sorted alphabetically. + """ + # Verified providers always included (includes custom like 'openhands') + verified_providers = set(VERIFIED_MODELS.keys()) + + # Unverified providers are filtered to only valid litellm providers + unverified_providers = set(UNVERIFIED_MODELS_EXCLUDING_BEDROCK.keys()) + valid_unverified = unverified_providers & _VALID_LITELLM_PROVIDERS + + # Combine and sort + all_valid_providers = sorted(verified_providers | valid_unverified) + + return [(provider, provider) for provider in all_valid_providers] def get_model_options(provider: str) -> list[tuple[str, str]]: - """Get list of available models for a provider.""" + """Get list of available models for a provider, sorted alphabetically.""" models = VERIFIED_MODELS.get( provider, [] ) + UNVERIFIED_MODELS_EXCLUDING_BEDROCK.get(provider, []) - return [(model, model) for model in models] + + # Remove duplicates and sort + unique_models = sorted(set(models)) + + return [(model, model) for model in unique_models] provider_options = get_provider_options() diff --git a/openhands_cli/tui/modals/settings/utils.py b/openhands_cli/tui/modals/settings/utils.py index 7c7d761a..5fa36641 100644 --- a/openhands_cli/tui/modals/settings/utils.py +++ b/openhands_cli/tui/modals/settings/utils.py @@ -79,10 +79,11 @@ def get_full_model_name(self): return str(self.custom_model) model_str = str(self.model) - full_model = ( - f"{self.provider}/{model_str}" if "/" not in model_str else model_str - ) - return full_model + + # Always add provider prefix - litellm requires it for routing. + # Even if model contains '/' (e.g. "openai/gpt-4.1" from openrouter) + # See: https://docs.litellm.ai/docs/providers + return f"{self.provider}/{model_str}" class SettingsSaveResult(BaseModel): diff --git a/tests/tui/modals/settings/test_settings_utils.py b/tests/tui/modals/settings/test_settings_utils.py index 93fa1173..c870bfb6 100644 --- a/tests/tui/modals/settings/test_settings_utils.py +++ b/tests/tui/modals/settings/test_settings_utils.py @@ -132,6 +132,40 @@ def test_preserves_existing_api_key_when_not_provided( None, # advanced base_url cleared ("custom_model", "base_url"), ), + ( + "basic", + "openrouter", + "google/gemini-3-flash-preview", + "should-be-cleared", + "https://advanced.example", + # All providers require prefix even for models with '/' in their name + # See: https://docs.litellm.ai/docs/providers/openrouter + "openrouter/google/gemini-3-flash-preview", + None, + ("custom_model", "base_url"), + ), + ( + "basic", + "nvidia_nim", + "meta/llama3-70b-instruct", + "should-be-cleared", + "https://advanced.example", + # See: https://docs.litellm.ai/docs/providers/nvidia_nim + "nvidia_nim/meta/llama3-70b-instruct", + None, + ("custom_model", "base_url"), + ), + ( + "basic", + "deepinfra", + "meta-llama/Meta-Llama-3.1-8B-Instruct", + "should-be-cleared", + "https://advanced.example", + # See: https://docs.litellm.ai/docs/providers + "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct", + None, + ("custom_model", "base_url"), + ), ( "advanced", "openai",