mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
fix: add missing verifiedModels/verifiedProviders props in tests, run ruff format
- model-selector.test.tsx: pass verifiedModels and verifiedProviders to every <ModelSelector /> render call - settings-form.test.tsx: pass verifiedModels and verifiedProviders to <SettingsForm /> in test fixture - Run ruff format on all changed Python files (single→double quotes) - Fix D202 blank-line-after-docstring in public.py Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -19,18 +19,18 @@ from openhands.app_server.config import get_db_session
|
||||
from openhands.server.routes import public
|
||||
from openhands.utils.llm import ModelsResponse, get_supported_llm_models
|
||||
|
||||
api_router = APIRouter(prefix='/api/admin/verified-models', tags=['Verified Models'])
|
||||
api_router = APIRouter(prefix="/api/admin/verified-models", tags=["Verified Models"])
|
||||
|
||||
|
||||
@api_router.get('')
|
||||
@api_router.get("")
|
||||
async def search_verified_models(
|
||||
provider: str | None = None,
|
||||
page_id: Annotated[
|
||||
str | None,
|
||||
Query(title='Optional next_page_id from the previously returned page'),
|
||||
Query(title="Optional next_page_id from the previously returned page"),
|
||||
] = None,
|
||||
limit: Annotated[
|
||||
int, Query(title='The max number of results in the page', gt=0, le=100)
|
||||
int, Query(title="The max number of results in the page", gt=0, le=100)
|
||||
] = 100,
|
||||
user_id: str = Depends(get_admin_user_id),
|
||||
verified_model_service: VerifiedModelService = Depends(
|
||||
@@ -48,7 +48,7 @@ async def search_verified_models(
|
||||
return result
|
||||
|
||||
|
||||
@api_router.post('', status_code=201)
|
||||
@api_router.post("", status_code=201)
|
||||
async def create_verified_model(
|
||||
data: VerifiedModelCreate,
|
||||
user_id: str = Depends(get_admin_user_id),
|
||||
@@ -71,7 +71,7 @@ async def create_verified_model(
|
||||
)
|
||||
|
||||
|
||||
@api_router.put('/{provider}/{model_name:path}')
|
||||
@api_router.put("/{provider}/{model_name:path}")
|
||||
async def update_verified_model(
|
||||
provider: str,
|
||||
model_name: str,
|
||||
@@ -90,12 +90,12 @@ async def update_verified_model(
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f'Model {provider}/{model_name} not found',
|
||||
detail=f"Model {provider}/{model_name} not found",
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
@api_router.delete('/{provider}/{model_name:path}')
|
||||
@api_router.delete("/{provider}/{model_name:path}")
|
||||
async def delete_verified_model(
|
||||
provider: str,
|
||||
model_name: str,
|
||||
@@ -128,9 +128,9 @@ async def get_saas_llm_models_dependency(request: Request) -> ModelsResponse:
|
||||
if page.next_page_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Too many models defined in database',
|
||||
detail="Too many models defined in database",
|
||||
)
|
||||
verified_models = [f'{m.provider}/{m.model_name}' for m in page.items]
|
||||
verified_models = [f"{m.provider}/{m.model_name}" for m in page.items]
|
||||
return get_supported_llm_models(config, verified_models)
|
||||
|
||||
|
||||
|
||||
@@ -38,9 +38,18 @@ describe("ModelSelector", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const verifiedModels = ["gpt-4o", "gpt-4o-mini"];
|
||||
const verifiedProviders = ["openai"];
|
||||
|
||||
it("should display the provider selector", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelSelector models={models} />);
|
||||
render(
|
||||
<ModelSelector
|
||||
models={models}
|
||||
verifiedModels={verifiedModels}
|
||||
verifiedProviders={verifiedProviders}
|
||||
/>,
|
||||
);
|
||||
|
||||
const selector = screen.getByLabelText("LLM Provider");
|
||||
expect(selector).toBeInTheDocument();
|
||||
@@ -55,7 +64,13 @@ describe("ModelSelector", () => {
|
||||
|
||||
it("should disable the model selector if the provider is not selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelSelector models={models} />);
|
||||
render(
|
||||
<ModelSelector
|
||||
models={models}
|
||||
verifiedModels={verifiedModels}
|
||||
verifiedProviders={verifiedProviders}
|
||||
/>,
|
||||
);
|
||||
|
||||
const modelSelector = screen.getByLabelText("LLM Model");
|
||||
expect(modelSelector).toBeDisabled();
|
||||
@@ -71,7 +86,13 @@ describe("ModelSelector", () => {
|
||||
|
||||
it("should display the model selector", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelSelector models={models} />);
|
||||
render(
|
||||
<ModelSelector
|
||||
models={models}
|
||||
verifiedModels={verifiedModels}
|
||||
verifiedProviders={verifiedProviders}
|
||||
/>,
|
||||
);
|
||||
|
||||
const providerSelector = screen.getByLabelText("LLM Provider");
|
||||
await user.click(providerSelector);
|
||||
@@ -101,7 +122,13 @@ describe("ModelSelector", () => {
|
||||
|
||||
it("should call onModelChange when the model is changed", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelSelector models={models} />);
|
||||
render(
|
||||
<ModelSelector
|
||||
models={models}
|
||||
verifiedModels={verifiedModels}
|
||||
verifiedProviders={verifiedProviders}
|
||||
/>,
|
||||
);
|
||||
|
||||
const providerSelector = screen.getByLabelText("LLM Provider");
|
||||
const modelSelector = screen.getByLabelText("LLM Model");
|
||||
@@ -128,7 +155,14 @@ describe("ModelSelector", () => {
|
||||
});
|
||||
|
||||
it("should have a default value if passed", async () => {
|
||||
render(<ModelSelector models={models} currentModel="azure/ada" />);
|
||||
render(
|
||||
<ModelSelector
|
||||
models={models}
|
||||
verifiedModels={verifiedModels}
|
||||
verifiedProviders={verifiedProviders}
|
||||
currentModel="azure/ada"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("LLM Provider")).toHaveValue("Azure");
|
||||
expect(screen.getByLabelText("LLM Model")).toHaveValue("ada");
|
||||
|
||||
@@ -17,6 +17,8 @@ describe("SettingsForm", () => {
|
||||
<SettingsForm
|
||||
settings={DEFAULT_SETTINGS}
|
||||
models={[DEFAULT_SETTINGS.llm_model]}
|
||||
verifiedModels={[]}
|
||||
verifiedProviders={["openhands"]}
|
||||
onClose={onCloseMock}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -15,7 +15,7 @@ from openhands.utils.shutdown_listener import should_continue
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.websocket('/ws')
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||
await websocket.accept()
|
||||
|
||||
@@ -23,58 +23,67 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
||||
while should_continue():
|
||||
# receive message
|
||||
data = await websocket.receive_json()
|
||||
logger.debug(f'Received message: {data}')
|
||||
logger.debug(f"Received message: {data}")
|
||||
|
||||
# send mock response to client
|
||||
response = {'message': f'receive {data}'}
|
||||
response = {"message": f"receive {data}"}
|
||||
await websocket.send_json(response)
|
||||
logger.debug(f'Sent message: {response}')
|
||||
logger.debug(f"Sent message: {response}")
|
||||
except Exception as e:
|
||||
logger.debug(f'WebSocket Error: {e}')
|
||||
logger.debug(f"WebSocket Error: {e}")
|
||||
|
||||
|
||||
@app.get('/')
|
||||
@app.get("/")
|
||||
def read_root() -> dict[str, str]:
|
||||
return {'message': 'This is a mock server'}
|
||||
return {"message": "This is a mock server"}
|
||||
|
||||
|
||||
@app.get('/api/options/models')
|
||||
@app.get("/api/options/models")
|
||||
def read_llm_models() -> dict:
|
||||
return {
|
||||
'models': [
|
||||
'openai/gpt-4',
|
||||
'openai/gpt-4-turbo-preview',
|
||||
'openai/gpt-4-0314',
|
||||
'openai/gpt-4-0613',
|
||||
"models": [
|
||||
"openai/gpt-4",
|
||||
"openai/gpt-4-turbo-preview",
|
||||
"openai/gpt-4-0314",
|
||||
"openai/gpt-4-0613",
|
||||
],
|
||||
'verified_models': [],
|
||||
'verified_providers': ['openhands', 'anthropic', 'openai', 'mistral', 'gemini', 'deepseek', 'moonshot', 'minimax'],
|
||||
'default_model': 'openhands/claude-opus-4-5-20251101',
|
||||
"verified_models": [],
|
||||
"verified_providers": [
|
||||
"openhands",
|
||||
"anthropic",
|
||||
"openai",
|
||||
"mistral",
|
||||
"gemini",
|
||||
"deepseek",
|
||||
"moonshot",
|
||||
"minimax",
|
||||
],
|
||||
"default_model": "openhands/claude-opus-4-5-20251101",
|
||||
}
|
||||
|
||||
|
||||
@app.get('/api/options/agents')
|
||||
@app.get("/api/options/agents")
|
||||
def read_llm_agents() -> list[str]:
|
||||
return [
|
||||
'CodeActAgent',
|
||||
"CodeActAgent",
|
||||
]
|
||||
|
||||
|
||||
@app.get('/api/list-files')
|
||||
@app.get("/api/list-files")
|
||||
def refresh_files() -> list[str]:
|
||||
return ['hello_world.py']
|
||||
return ["hello_world.py"]
|
||||
|
||||
|
||||
@app.get('/api/options/config')
|
||||
@app.get("/api/options/config")
|
||||
def get_config() -> dict[str, str]:
|
||||
# return {'APP_MODE': 'oss'}
|
||||
return {'APP_MODE': 'saas'}
|
||||
return {"APP_MODE": "saas"}
|
||||
|
||||
|
||||
@app.get('/api/options/security-analyzers')
|
||||
@app.get("/api/options/security-analyzers")
|
||||
def get_analyzers() -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(app, host='127.0.0.1', port=3000)
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=3000)
|
||||
|
||||
@@ -17,7 +17,7 @@ from openhands.server.dependencies import get_dependencies
|
||||
from openhands.server.shared import config, server_config
|
||||
from openhands.utils.llm import ModelsResponse, get_supported_llm_models
|
||||
|
||||
app = APIRouter(prefix='/api/options', dependencies=get_dependencies())
|
||||
app = APIRouter(prefix="/api/options", dependencies=get_dependencies())
|
||||
|
||||
|
||||
async def get_llm_models_dependency(request: Request) -> ModelsResponse:
|
||||
@@ -26,18 +26,17 @@ async def get_llm_models_dependency(request: Request) -> ModelsResponse:
|
||||
Returns a factory that produces the actual implementation function.
|
||||
Override this in enterprise/saas mode via app.dependency_overrides.
|
||||
"""
|
||||
|
||||
return get_supported_llm_models(config)
|
||||
|
||||
|
||||
@app.get('/models')
|
||||
@app.get("/models")
|
||||
async def get_litellm_models(
|
||||
models: ModelsResponse = Depends(get_llm_models_dependency),
|
||||
) -> ModelsResponse:
|
||||
return models
|
||||
|
||||
|
||||
@app.get('/agents', response_model=list[str])
|
||||
@app.get("/agents", response_model=list[str])
|
||||
async def get_agents() -> list[str]:
|
||||
"""Get all agents supported by LiteLLM.
|
||||
|
||||
@@ -52,7 +51,7 @@ async def get_agents() -> list[str]:
|
||||
return sorted(Agent.list_agents())
|
||||
|
||||
|
||||
@app.get('/security-analyzers', response_model=list[str])
|
||||
@app.get("/security-analyzers", response_model=list[str])
|
||||
async def get_security_analyzers() -> list[str]:
|
||||
"""Get all supported security analyzers.
|
||||
|
||||
@@ -67,7 +66,7 @@ async def get_security_analyzers() -> list[str]:
|
||||
return sorted(SecurityAnalyzers.keys())
|
||||
|
||||
|
||||
@app.get('/config', response_model=dict[str, Any], deprecated=True)
|
||||
@app.get("/config", response_model=dict[str, Any], deprecated=True)
|
||||
async def get_config() -> dict[str, Any]:
|
||||
"""Get current config.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
warnings.simplefilter("ignore")
|
||||
import litellm
|
||||
from litellm import LlmProviders, ProviderConfigManager, get_llm_provider
|
||||
|
||||
@@ -29,22 +29,22 @@ from openhands.sdk.llm.utils.verified_models import ( # noqa: E402
|
||||
)
|
||||
|
||||
# Build the ``openhands/…`` model list from the SDK.
|
||||
OPENHANDS_MODELS: list[str] = [f'openhands/{m}' for m in _SDK_OPENHANDS]
|
||||
OPENHANDS_MODELS: list[str] = [f"openhands/{m}" for m in _SDK_OPENHANDS]
|
||||
|
||||
CLARIFAI_MODELS = [
|
||||
'clarifai/openai.chat-completion.gpt-oss-120b',
|
||||
'clarifai/openai.chat-completion.gpt-oss-20b',
|
||||
'clarifai/openai.chat-completion.gpt-5',
|
||||
'clarifai/openai.chat-completion.gpt-5-mini',
|
||||
'clarifai/qwen.qwen3.qwen3-next-80B-A3B-Thinking',
|
||||
'clarifai/qwen.qwenLM.Qwen3-30B-A3B-Instruct-2507',
|
||||
'clarifai/qwen.qwenLM.Qwen3-30B-A3B-Thinking-2507',
|
||||
'clarifai/qwen.qwenLM.Qwen3-14B',
|
||||
'clarifai/qwen.qwenCoder.Qwen3-Coder-30B-A3B-Instruct',
|
||||
'clarifai/deepseek-ai.deepseek-chat.DeepSeek-R1-0528-Qwen3-8B',
|
||||
'clarifai/deepseek-ai.deepseek-chat.DeepSeek-V3_1',
|
||||
'clarifai/zai.completion.GLM_4_5',
|
||||
'clarifai/moonshotai.kimi.Kimi-K2-Instruct',
|
||||
"clarifai/openai.chat-completion.gpt-oss-120b",
|
||||
"clarifai/openai.chat-completion.gpt-oss-20b",
|
||||
"clarifai/openai.chat-completion.gpt-5",
|
||||
"clarifai/openai.chat-completion.gpt-5-mini",
|
||||
"clarifai/qwen.qwen3.qwen3-next-80B-A3B-Thinking",
|
||||
"clarifai/qwen.qwenLM.Qwen3-30B-A3B-Instruct-2507",
|
||||
"clarifai/qwen.qwenLM.Qwen3-30B-A3B-Thinking-2507",
|
||||
"clarifai/qwen.qwenLM.Qwen3-14B",
|
||||
"clarifai/qwen.qwenCoder.Qwen3-Coder-30B-A3B-Instruct",
|
||||
"clarifai/deepseek-ai.deepseek-chat.DeepSeek-R1-0528-Qwen3-8B",
|
||||
"clarifai/deepseek-ai.deepseek-chat.DeepSeek-V3_1",
|
||||
"clarifai/zai.completion.GLM_4_5",
|
||||
"clarifai/moonshotai.kimi.Kimi-K2-Instruct",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -60,7 +60,7 @@ _BARE_OPENAI_MODELS: set[str] = set(_SDK_OPENAI)
|
||||
_BARE_ANTHROPIC_MODELS: set[str] = set(_SDK_ANTHROPIC)
|
||||
_BARE_MISTRAL_MODELS: set[str] = set(_SDK_MISTRAL)
|
||||
|
||||
DEFAULT_OPENHANDS_MODEL = 'openhands/claude-opus-4-5-20251101'
|
||||
DEFAULT_OPENHANDS_MODEL = "openhands/claude-opus-4-5-20251101"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -93,7 +93,7 @@ def is_openhands_model(model: str | None) -> bool:
|
||||
Returns:
|
||||
True if the model starts with 'openhands/', False otherwise.
|
||||
"""
|
||||
return bool(model and model.startswith('openhands/'))
|
||||
return bool(model and model.startswith("openhands/"))
|
||||
|
||||
|
||||
def get_provider_api_base(model: str) -> str | None:
|
||||
@@ -129,7 +129,7 @@ def get_provider_api_base(model: str) -> str | None:
|
||||
model_info = ProviderConfigManager.get_provider_model_info(
|
||||
model, provider_enum
|
||||
)
|
||||
if model_info and hasattr(model_info, 'get_api_base'):
|
||||
if model_info and hasattr(model_info, "get_api_base"):
|
||||
return model_info.get_api_base()
|
||||
except ValueError:
|
||||
pass # Provider not in enum
|
||||
@@ -165,26 +165,26 @@ def _assign_provider(model: str) -> str:
|
||||
unchanged. Only well-known bare names (OpenAI, Anthropic, Mistral,
|
||||
OpenHands) are prefixed.
|
||||
"""
|
||||
if '/' in model or '.' in model:
|
||||
if "/" in model or "." in model:
|
||||
return model
|
||||
|
||||
# Build the openhands bare-name set dynamically so it always matches
|
||||
# whatever ``get_openhands_models`` returns at call time.
|
||||
if model in _BARE_OPENAI_MODELS:
|
||||
return f'openai/{model}'
|
||||
return f"openai/{model}"
|
||||
if model in _BARE_ANTHROPIC_MODELS:
|
||||
return f'anthropic/{model}'
|
||||
return f"anthropic/{model}"
|
||||
if model in _BARE_MISTRAL_MODELS:
|
||||
return f'mistral/{model}'
|
||||
return f"mistral/{model}"
|
||||
return model
|
||||
|
||||
|
||||
def _derive_verified_models(openhands_models: list[str]) -> list[str]:
|
||||
"""Extract the bare model names from the ``openhands/…`` model list."""
|
||||
return [
|
||||
m.removeprefix('openhands/')
|
||||
m.removeprefix("openhands/")
|
||||
for m in openhands_models
|
||||
if m.startswith('openhands/')
|
||||
if m.startswith("openhands/")
|
||||
]
|
||||
|
||||
|
||||
@@ -228,25 +228,25 @@ def get_supported_llm_models(
|
||||
model_list = litellm_model_list_without_bedrock + bedrock_model_list
|
||||
for llm_config in config.llms.values():
|
||||
ollama_base_url = llm_config.ollama_base_url
|
||||
if llm_config.model.startswith('ollama'):
|
||||
if llm_config.model.startswith("ollama"):
|
||||
if not ollama_base_url:
|
||||
ollama_base_url = llm_config.base_url
|
||||
if ollama_base_url:
|
||||
ollama_url = ollama_base_url.strip('/') + '/api/tags'
|
||||
ollama_url = ollama_base_url.strip("/") + "/api/tags"
|
||||
try:
|
||||
ollama_models_list = httpx.get(ollama_url, timeout=3).json()['models'] # noqa: ASYNC100
|
||||
ollama_models_list = httpx.get(ollama_url, timeout=3).json()["models"] # noqa: ASYNC100
|
||||
for model in ollama_models_list:
|
||||
model_list.append('ollama/' + model['name'])
|
||||
model_list.append("ollama/" + model["name"])
|
||||
break
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f'Error getting OLLAMA models: {e}')
|
||||
logger.error(f"Error getting OLLAMA models: {e}")
|
||||
|
||||
openhands_models = get_openhands_models(verified_models)
|
||||
|
||||
# Assign canonical provider prefixes to bare LiteLLM names, then dedupe.
|
||||
all_models = openhands_models + CLARIFAI_MODELS + [
|
||||
_assign_provider(m) for m in model_list
|
||||
]
|
||||
all_models = (
|
||||
openhands_models + CLARIFAI_MODELS + [_assign_provider(m) for m in model_list]
|
||||
)
|
||||
unique_models = sorted(set(all_models))
|
||||
|
||||
return ModelsResponse(
|
||||
|
||||
Reference in New Issue
Block a user