fix: switching from own model to breaks functionality (#11916)

This commit is contained in:
Hiep Le 2025-12-06 11:21:18 +07:00 committed by GitHub
parent 72c7d9c497
commit d7b36c9579
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 1 deletions

View File

@ -102,6 +102,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
sandbox_startup_poll_frequency: int
httpx_client: httpx.AsyncClient
web_url: str | None
openhands_provider_base_url: str | None
access_token_hard_timeout: timedelta | None
app_mode: str | None = None
keycloak_auth_cookie: str | None = None
@ -590,9 +591,12 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
"""
# Configure LLM
model = llm_model or user.llm_model
base_url = user.llm_base_url
if model and model.startswith('openhands/'):
base_url = user.llm_base_url or self.openhands_provider_base_url
llm = LLM(
model=model,
base_url=user.llm_base_url,
base_url=base_url,
api_key=user.llm_api_key,
usage_id='agent',
)
@ -1082,6 +1086,7 @@ class LiveStatusAppConversationServiceInjector(AppConversationServiceInjector):
sandbox_startup_poll_frequency=self.sandbox_startup_poll_frequency,
httpx_client=httpx_client,
web_url=web_url,
openhands_provider_base_url=config.openhands_provider_base_url,
access_token_hard_timeout=access_token_hard_timeout,
app_mode=app_mode,
keycloak_auth_cookie=keycloak_auth_cookie,

View File

@ -74,6 +74,11 @@ def get_default_web_url() -> str | None:
return f'https://{web_host}'
def get_openhands_provider_base_url() -> str | None:
"""Return the base URL for the OpenHands provider, if configured."""
return os.getenv('OPENHANDS_PROVIDER_BASE_URL') or None
def _get_default_lifespan():
# Check legacy parameters for saas mode. If we are in SAAS mode do not apply
# OSS alembic migrations
@ -88,6 +93,10 @@ class AppServerConfig(OpenHandsModel):
default_factory=get_default_web_url,
description='The URL where OpenHands is running (e.g., http://localhost:3000)',
)
openhands_provider_base_url: str | None = Field(
default_factory=get_openhands_provider_base_url,
description='Base URL for the OpenHands provider',
)
# Dependency Injection Injectors
event: EventServiceInjector | None = None
event_callback: EventCallbackServiceInjector | None = None

View File

@ -52,6 +52,7 @@ class TestLiveStatusAppConversationService:
sandbox_startup_poll_frequency=1,
httpx_client=self.mock_httpx_client,
web_url='https://test.example.com',
openhands_provider_base_url='https://provider.example.com',
access_token_hard_timeout=None,
app_mode='test',
keycloak_auth_cookie=None,
@ -66,6 +67,7 @@ class TestLiveStatusAppConversationService:
self.mock_user.confirmation_mode = False
self.mock_user.search_api_key = None # Default to None
self.mock_user.condenser_max_size = None # Default to None
self.mock_user.llm_base_url = 'https://api.openai.com/v1'
# Mock sandbox
self.mock_sandbox = Mock(spec=SandboxInfo)
@ -241,6 +243,70 @@ class TestLiveStatusAppConversationService:
assert mcp_config['default']['url'] == 'https://test.example.com/mcp/mcp'
assert mcp_config['default']['headers']['X-Session-API-Key'] == 'mcp_api_key'
@pytest.mark.asyncio
async def test_configure_llm_and_mcp_openhands_model_prefers_user_base_url(self):
"""openhands/* model uses user.llm_base_url when provided."""
# Arrange
self.mock_user.llm_model = 'openhands/special'
self.mock_user.llm_base_url = 'https://user-llm.example.com'
self.mock_user_context.get_mcp_api_key.return_value = None
# Act
llm, _ = await self.service._configure_llm_and_mcp(
self.mock_user, self.mock_user.llm_model
)
# Assert
assert llm.base_url == 'https://user-llm.example.com'
@pytest.mark.asyncio
async def test_configure_llm_and_mcp_openhands_model_uses_provider_default(self):
"""openhands/* model falls back to configured provider base URL."""
# Arrange
self.mock_user.llm_model = 'openhands/default'
self.mock_user.llm_base_url = None
self.mock_user_context.get_mcp_api_key.return_value = None
# Act
llm, _ = await self.service._configure_llm_and_mcp(
self.mock_user, self.mock_user.llm_model
)
# Assert
assert llm.base_url == 'https://provider.example.com'
@pytest.mark.asyncio
async def test_configure_llm_and_mcp_openhands_model_no_base_urls(self):
"""openhands/* model sets base_url to None when no sources available."""
# Arrange
self.mock_user.llm_model = 'openhands/default'
self.mock_user.llm_base_url = None
self.service.openhands_provider_base_url = None
self.mock_user_context.get_mcp_api_key.return_value = None
# Act
llm, _ = await self.service._configure_llm_and_mcp(
self.mock_user, self.mock_user.llm_model
)
# Assert
assert llm.base_url is None
@pytest.mark.asyncio
async def test_configure_llm_and_mcp_non_openhands_model_ignores_provider(self):
"""Non-openhands model ignores provider base URL and uses user base URL."""
# Arrange
self.mock_user.llm_model = 'gpt-4'
self.mock_user.llm_base_url = 'https://user-llm.example.com'
self.service.openhands_provider_base_url = 'https://provider.example.com'
self.mock_user_context.get_mcp_api_key.return_value = None
# Act
llm, _ = await self.service._configure_llm_and_mcp(self.mock_user, None)
# Assert
assert llm.base_url == 'https://user-llm.example.com'
@pytest.mark.asyncio
async def test_configure_llm_and_mcp_with_user_default_model(self):
"""Test _configure_llm_and_mcp using user's default model."""

View File

@ -188,6 +188,7 @@ class TestExperimentManagerIntegration:
sandbox_startup_poll_frequency=1,
httpx_client=httpx_client,
web_url=None,
openhands_provider_base_url=None,
access_token_hard_timeout=None,
)

View File

@ -2166,6 +2166,7 @@ async def test_delete_v1_conversation_with_sub_conversations():
sandbox_startup_poll_frequency=2,
httpx_client=mock_httpx_client,
web_url=None,
openhands_provider_base_url=None,
access_token_hard_timeout=None,
)
@ -2287,6 +2288,7 @@ async def test_delete_v1_conversation_with_no_sub_conversations():
sandbox_startup_poll_frequency=2,
httpx_client=mock_httpx_client,
web_url=None,
openhands_provider_base_url=None,
access_token_hard_timeout=None,
)
@ -2438,6 +2440,7 @@ async def test_delete_v1_conversation_sub_conversation_deletion_error():
sandbox_startup_poll_frequency=2,
httpx_client=mock_httpx_client,
web_url=None,
openhands_provider_base_url=None,
access_token_hard_timeout=None,
)