diff --git a/src/langbot/pkg/api/http/controller/groups/provider/models.py b/src/langbot/pkg/api/http/controller/groups/provider/models.py index 236000d9f..5d17d7ffc 100644 --- a/src/langbot/pkg/api/http/controller/groups/provider/models.py +++ b/src/langbot/pkg/api/http/controller/groups/provider/models.py @@ -1,10 +1,14 @@ import quart +from langbot.pkg.provider.modelmgr import errors as provider_errors from ....authz import Permission, has_permission from ....context import RequestContext from ... import group +MODEL_TEST_ERROR_CODE = 'model_test_failed' + + @group.group_class('models/llm', '/api/v1/provider/models/llm') class LLMModelsRouterGroup(group.RouterGroup): async def initialize(self) -> None: @@ -96,7 +100,14 @@ class LLMModelsRouterGroup(group.RouterGroup): permission=Permission.PROVIDER_SECRET_MANAGE, ) async def _(model_uuid: str, request_context: RequestContext) -> str: - await self.ap.llm_model_service.test_llm_model(request_context, model_uuid, await quart.request.json) + try: + await self.ap.llm_model_service.test_llm_model( + request_context, + model_uuid, + await quart.request.json, + ) + except (provider_errors.RequesterError, ValueError) as exc: + return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc)) return self.success() @@ -191,9 +202,14 @@ class EmbeddingModelsRouterGroup(group.RouterGroup): permission=Permission.PROVIDER_SECRET_MANAGE, ) async def _(model_uuid: str, request_context: RequestContext) -> str: - await self.ap.embedding_models_service.test_embedding_model( - request_context, model_uuid, await quart.request.json - ) + try: + await self.ap.embedding_models_service.test_embedding_model( + request_context, + model_uuid, + await quart.request.json, + ) + except (provider_errors.RequesterError, ValueError) as exc: + return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc)) return self.success() @@ -288,5 +304,12 @@ class RerankModelsRouterGroup(group.RouterGroup): permission=Permission.PROVIDER_SECRET_MANAGE, ) async def _(model_uuid: str, request_context: RequestContext) -> str: - await self.ap.rerank_models_service.test_rerank_model(request_context, model_uuid, await quart.request.json) + try: + await self.ap.rerank_models_service.test_rerank_model( + request_context, + model_uuid, + await quart.request.json, + ) + except (provider_errors.RequesterError, ValueError) as exc: + return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc)) return self.success() diff --git a/tests/integration/api/test_providers.py b/tests/integration/api/test_providers.py index 4aa2e1342..df72d9084 100644 --- a/tests/integration/api/test_providers.py +++ b/tests/integration/api/test_providers.py @@ -12,6 +12,7 @@ import pytest from unittest.mock import MagicMock, AsyncMock, Mock from types import SimpleNamespace +from langbot.pkg.provider.modelmgr.errors import RequesterError from tests.factories import FakeApp @@ -111,16 +112,19 @@ def fake_provider_app(): app.llm_model_service.create_llm_model = AsyncMock(return_value={'uuid': 'new-model-uuid'}) app.llm_model_service.update_llm_model = AsyncMock(return_value={}) app.llm_model_service.delete_llm_model = AsyncMock() + app.llm_model_service.test_llm_model = AsyncMock() # Embedding model service app.embedding_models_service = Mock() app.embedding_models_service.get_embedding_models = AsyncMock(return_value=[]) app.embedding_models_service.create_embedding_model = AsyncMock(return_value={'uuid': 'new-embedding-uuid'}) + app.embedding_models_service.test_embedding_model = AsyncMock() # Rerank model service app.rerank_models_service = Mock() app.rerank_models_service.get_rerank_models = AsyncMock(return_value=[]) app.rerank_models_service.create_rerank_model = AsyncMock(return_value={'uuid': 'new-rerank-uuid'}) + app.rerank_models_service.test_rerank_model = AsyncMock() # Model manager app.model_mgr = Mock() @@ -290,6 +294,43 @@ class TestModelEndpoints: assert response.status_code == 200 + @pytest.mark.asyncio + @pytest.mark.parametrize( + ('service_name', 'method_name', 'path'), + [ + ('llm_model_service', 'test_llm_model', '/api/v1/provider/models/llm/_/test'), + ( + 'embedding_models_service', + 'test_embedding_model', + '/api/v1/provider/models/embedding/_/test', + ), + ('rerank_models_service', 'test_rerank_model', '/api/v1/provider/models/rerank/_/test'), + ], + ) + async def test_model_test_returns_requester_error_details( + self, + quart_test_client, + fake_provider_app, + service_name, + method_name, + path, + ): + test_model = getattr(getattr(fake_provider_app, service_name), method_name) + test_model.side_effect = RequesterError('API key invalid') + try: + response = await quart_test_client.post( + path, + headers={'Authorization': 'Bearer test_token'}, + json={'name': 'broken-model'}, + ) + finally: + test_model.side_effect = None + + assert response.status_code == 400 + data = await response.get_json() + assert data['code'] == 'model_test_failed' + assert data['msg'] == '模型请求失败: API key invalid' + @pytest.mark.usefixtures('mock_circular_import_chain') class TestEmbeddingModelEndpoints: diff --git a/web/src/app/home/agents/components/AgentDebugPanel.tsx b/web/src/app/home/agents/components/AgentDebugPanel.tsx index 913d7ecc3..2cdbe06c6 100644 --- a/web/src/app/home/agents/components/AgentDebugPanel.tsx +++ b/web/src/app/home/agents/components/AgentDebugPanel.tsx @@ -312,48 +312,55 @@ export default function AgentDebugPanel({ } > {entry.direction === 'error' && } -
- {entry.eventType} - - {entry.direction === 'output' - ? t('agents.debugAgentOutput') - : entry.direction === 'error' - ? t('common.error') - : t('agents.debugTestInput')} - -
-
-                  {entry.text}
-                
- {entry.detail && ( - - - - - -
-                        {entry.detail}
-                      
-
-
- )} - {(entry.errorCode?.endsWith('.config_invalid') || - entry.errorCode === 'runner_execution_failed' || - entry.errorCode === 'runner.timeout') && - onOpenRunnerConfig && ( - + {entry.eventType} + + + {entry.direction === 'output' + ? t('agents.debugAgentOutput') + : entry.direction === 'error' + ? t('common.error') + : t('agents.debugTestInput')} + + +
+                    {entry.text}
+                  
+ {entry.detail && ( + + + + + +
+                          {entry.detail}
+                        
+
+
)} + {(entry.errorCode?.endsWith('.config_invalid') || + entry.errorCode === 'runner_execution_failed' || + entry.errorCode === 'runner.timeout') && + onOpenRunnerConfig && ( + + )} + ))} diff --git a/web/src/app/home/components/models-dialog/ModelsPanel.tsx b/web/src/app/home/components/models-dialog/ModelsPanel.tsx index c8d8cfdda..0292ef630 100644 --- a/web/src/app/home/components/models-dialog/ModelsPanel.tsx +++ b/web/src/app/home/components/models-dialog/ModelsPanel.tsx @@ -522,8 +522,9 @@ export default function ModelsPanel({ setTestResult({ success: true, duration }); } catch (err) { console.error('Failed to test model', err); - toast.error(t('models.testError') + ': ' + (err as CustomApiError).msg); - setTestResult(null); + const message = (err as CustomApiError).msg || t('models.testError'); + toast.error(t('models.testError') + ': ' + message); + setTestResult({ success: false, message }); } finally { setIsTesting(false); } diff --git a/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx index f12756749..bbf71c6a2 100644 --- a/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx +++ b/web/src/app/home/components/models-dialog/components/AddModelPopover.tsx @@ -437,6 +437,14 @@ export default function AddModelPopover({ )} + {testResult?.success === false && ( +

+ {testResult.message} +

+ )} ) : ( diff --git a/web/src/app/home/components/models-dialog/components/ModelItem.tsx b/web/src/app/home/components/models-dialog/components/ModelItem.tsx index 12e61da77..fae9be86f 100644 --- a/web/src/app/home/components/models-dialog/components/ModelItem.tsx +++ b/web/src/app/home/components/models-dialog/components/ModelItem.tsx @@ -412,6 +412,14 @@ export default function ModelItem({ )} + {testResult?.success === false && ( +

+ {testResult.message} +

+ )} diff --git a/web/src/app/home/components/models-dialog/types.ts b/web/src/app/home/components/models-dialog/types.ts index 8ed008e46..f8f72e0b1 100644 --- a/web/src/app/home/components/models-dialog/types.ts +++ b/web/src/app/home/components/models-dialog/types.ts @@ -27,10 +27,9 @@ export interface ProviderModels { rerank: RerankModel[]; } -export interface TestResult { - success: boolean; - duration: number; -} +export type TestResult = + | { success: true; duration: number } + | { success: false; message: string }; export type SelectedScannedModel = { model: ScannedProviderModel;