diff --git a/docs/openapi.json b/docs/openapi.json index 82ada88a9..f5695f94a 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -355,7 +355,7 @@ } ] }, - "400": { + "401": { "description": "Missing or invalid credentials provided by client", "content": { "application/json": { @@ -375,6 +375,9 @@ } } }, + "404": { + "description": "Requested model or provider not found" + }, "429": { "description": "The quota has been exceeded", "content": { diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 4993da1a6..2fbef609e 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -80,7 +80,7 @@ } ], }, - 400: { + 401: { "description": "Missing or invalid credentials provided by client", "model": UnauthorizedResponse, }, @@ -88,6 +88,9 @@ "description": "Client does not have permission to access conversation", "model": ForbiddenResponse, }, + 404: { + "description": "Requested model or provider not found", + }, 429: { "description": "The quota has been exceeded", "model": QuotaExceededResponse, @@ -486,6 +489,17 @@ def select_model_and_provider_id( Raises: HTTPException: If no suitable LLM model is found or the selected model is not available. """ + # If no models are available, raise an exception + if not models: + message = "No models available" + logger.error(message) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "response": constants.UNABLE_TO_PROCESS_RESPONSE, + "cause": message, + }, + ) # If model_id and provider_id are provided in the request, use them # If model_id is not provided in the request, check the configuration @@ -516,10 +530,10 @@ def select_model_and_provider_id( model_label = model_id.split("/", 1)[1] if "/" in model_id else model_id return model_id, model_label, provider_id except (StopIteration, AttributeError) as e: - message = "No LLM model found in available models" + message = "No models available" logger.error(message) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_404_NOT_FOUND, detail={ "response": constants.UNABLE_TO_PROCESS_RESPONSE, "cause": message, @@ -536,7 +550,7 @@ def select_model_and_provider_id( message = f"Model {model_id} from provider {provider_id} not found in available models" logger.error(message) raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_404_NOT_FOUND, detail={ "response": constants.UNABLE_TO_PROCESS_RESPONSE, "cause": message, diff --git a/src/authentication/jwk_token.py b/src/authentication/jwk_token.py index e10bf81a9..f9365f0c7 100644 --- a/src/authentication/jwk_token.py +++ b/src/authentication/jwk_token.py @@ -142,12 +142,12 @@ async def __call__(self, request: Request) -> AuthTuple: ) from exc except DecodeError as exc: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: decode error", ) from exc except JoseError as exc: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token: unknown error", ) from exc except Exception as exc: diff --git a/src/authentication/utils.py b/src/authentication/utils.py index c92898ac2..57ba51288 100644 --- a/src/authentication/utils.py +++ b/src/authentication/utils.py @@ -15,12 +15,12 @@ def extract_user_token(headers: Headers) -> str: """ authorization_header = headers.get("Authorization") if not authorization_header: - raise HTTPException(status_code=400, detail="No Authorization header found") + raise HTTPException(status_code=401, detail="No Authorization header found") scheme_and_token = authorization_header.strip().split() if len(scheme_and_token) != 2 or scheme_and_token[0].lower() != "bearer": raise HTTPException( - status_code=400, detail="No token found in Authorization header" + status_code=401, detail="No token found in Authorization header" ) return scheme_and_token[1] diff --git a/tests/e2e/config/no-models-run.yaml b/tests/e2e/config/no-models-run.yaml new file mode 100644 index 000000000..d5686c832 --- /dev/null +++ b/tests/e2e/config/no-models-run.yaml @@ -0,0 +1,30 @@ +--- +# A run configuration for lightspeed-stack that defines no models. +# Used for E2E testing. +llm: + mode: "library" + model_name: "" + model_provider: "" + api_key: "EMPTY" + parameters: + max_new_tokens: 256 + server: + host: "0.0.0.0" + port: 8080 + url: "http://llama-stack:8080" +lightspeed_stack: + server: + host: "0.0.0.0" + port: 8008 + logging: + level: "INFO" + style: "default" + model_defaults: + provider: "openai" + model: "gpt-4-turbo" + providers: + - name: "openai" + api_key: "EMPTY" + url: "http://llama-stack:8080" + type: "openai" + models: [] diff --git a/tests/e2e/features/authorized_noop_token.feature b/tests/e2e/features/authorized_noop_token.feature index 1169c6090..8c67d4732 100644 --- a/tests/e2e/features/authorized_noop_token.feature +++ b/tests/e2e/features/authorized_noop_token.feature @@ -11,7 +11,7 @@ Feature: Authorized endpoint API tests for the noop-with-token authentication mo """ {"placeholder":"abc"} """ - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ {"detail": "No Authorization header found"} diff --git a/tests/e2e/features/conversations.feature b/tests/e2e/features/conversations.feature index dfbea8fe8..9619961c5 100644 --- a/tests/e2e/features/conversations.feature +++ b/tests/e2e/features/conversations.feature @@ -34,7 +34,7 @@ Feature: conversations endpoint API tests And I store conversation details And I remove the auth header When I access REST API endpoint "conversations" using HTTP GET method - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ { @@ -100,7 +100,7 @@ Feature: conversations endpoint API tests And I store conversation details And I remove the auth header When I use REST API conversation endpoint with conversation_id from above using HTTP GET method - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ { diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 45f22cbb8..2a0c5f0de 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -78,6 +78,8 @@ def before_scenario(context: Context, scenario: Scenario) -> None: context.scenario_config = ( "tests/e2e/configuration/lightspeed-stack-invalid-feedback-storage.yaml" ) + if "no_models" in scenario.effective_tags: + context.scenario_config = "tests/e2e/config/no-models-run.yaml" def after_scenario(context: Context, scenario: Scenario) -> None: diff --git a/tests/e2e/features/feedback.feature b/tests/e2e/features/feedback.feature index 18a997147..060beab9b 100644 --- a/tests/e2e/features/feedback.feature +++ b/tests/e2e/features/feedback.feature @@ -108,7 +108,7 @@ Feature: feedback endpoint API tests And The body of the response is the following """ { - "detail": "Forbidden: User is not authorized to access this resource" + "detail": "Forbidden: Feedback is disabled" } """ @@ -246,7 +246,6 @@ Feature: feedback endpoint API tests Scenario: Check if feedback endpoint is not working when not authorized Given The system is in default state And A new conversation is initialized - And I remove the auth header When I submit the following feedback for the conversation created before """ { @@ -256,14 +255,11 @@ Feature: feedback endpoint API tests "user_question": "Sample Question" } """ - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ { - "detail": { - "cause": "Missing or invalid credentials provided by client", - "response": "Unauthorized" - } + "detail": "No Authorization header found" } """ @@ -271,14 +267,11 @@ Feature: feedback endpoint API tests Given The system is in default state And I remove the auth header When The feedback is enabled - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ { - "detail": { - "cause": "Missing or invalid credentials provided by client", - "response": "Unauthorized" - } + "detail": "No Authorization header found" } """ diff --git a/tests/e2e/features/query.feature b/tests/e2e/features/query.feature index 585546fb2..e4b5ddf81 100644 --- a/tests/e2e/features/query.feature +++ b/tests/e2e/features/query.feature @@ -53,12 +53,36 @@ Feature: Query endpoint API tests """ {"query": "Write a simple code for reversing string"} """ - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ {"detail": "No Authorization header found"} """ + Scenario: Check if LLM responds to sent question with error when authenticated with invalid token + Given The system is in default state + And I set the Authorization header to Bearer invalid + When I use "query" to ask question with authorization header + """ + {"query": "Write a simple code for reversing string"} + """ + Then The status code of the response is 401 + And The body of the response is the following + """ + {"detail":"Invalid token: decode error"} + """ + + Scenario: Check if LLM responds to sent question with error when model does not exist + Given The system is in default state + And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva + When I use "query" to ask question with authorization header + """ + {"query": "Write a simple code for reversing string", "model": "does-not-exist", "provider": "does-not-exist"} + """ + Then The status code of the response is 404 + And The body of the response contains Model does-not-exist from provider does-not-exist not found in available models + + Scenario: Check if LLM responds to sent question with error when attempting to access conversation Given The system is in default state And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -138,3 +162,4 @@ Scenario: Check if LLM responds for query request with error for missing query } """ Then The status code of the response is 200 + diff --git a/tests/e2e/features/streaming_query.feature b/tests/e2e/features/streaming_query.feature index 27196ccb7..0f74e1739 100644 --- a/tests/e2e/features/streaming_query.feature +++ b/tests/e2e/features/streaming_query.feature @@ -126,7 +126,7 @@ Feature: streaming_query endpoint API tests """ {"query": "Say hello"} """ - Then The status code of the response is 400 + Then The status code of the response is 401 And The body of the response is the following """ {"detail": "No Authorization header found"} diff --git a/tests/integration/test_openapi_json.py b/tests/integration/test_openapi_json.py index 8fe1c689f..adf616e04 100644 --- a/tests/integration/test_openapi_json.py +++ b/tests/integration/test_openapi_json.py @@ -129,7 +129,7 @@ def test_servers_section_present_from_url(spec_from_url: dict[str, Any]) -> None ("/v1/shields", "get", {"200", "500"}), ("/v1/providers", "get", {"200", "500"}), ("/v1/providers/{provider_id}", "get", {"200", "404", "422", "500"}), - ("/v1/query", "post", {"200", "400", "403", "500", "422"}), + ("/v1/query", "post", {"200", "401", "403", "404", "500", "422"}), ("/v1/streaming_query", "post", {"200", "400", "401", "403", "422", "500"}), ("/v1/config", "get", {"200", "503"}), ("/v1/feedback", "post", {"200", "401", "403", "500", "422"}), @@ -185,7 +185,7 @@ def test_paths_and_responses_exist_from_file( ("/v1/shields", "get", {"200", "500"}), ("/v1/providers", "get", {"200", "500"}), ("/v1/providers/{provider_id}", "get", {"200", "404", "422", "500"}), - ("/v1/query", "post", {"200", "400", "403", "500", "422"}), + ("/v1/query", "post", {"200", "401", "403", "404", "500", "422"}), ("/v1/streaming_query", "post", {"200", "400", "401", "403", "422", "500"}), ("/v1/config", "get", {"200", "503"}), ("/v1/feedback", "post", {"200", "401", "403", "500", "422"}), diff --git a/tests/unit/app/endpoints/test_authorized.py b/tests/unit/app/endpoints/test_authorized.py index ab92cb32c..d67c168b9 100644 --- a/tests/unit/app/endpoints/test_authorized.py +++ b/tests/unit/app/endpoints/test_authorized.py @@ -55,12 +55,12 @@ async def test_authorized_dependency_unauthorized() -> None: headers_no_auth = Headers({}) with pytest.raises(HTTPException) as exc_info: extract_user_token(headers_no_auth) - assert exc_info.value.status_code == 400 + assert exc_info.value.status_code == 401 assert exc_info.value.detail == "No Authorization header found" # Test case 2: Invalid Authorization header format (400 error from extract_user_token) headers_invalid_auth = Headers({"Authorization": "InvalidFormat"}) with pytest.raises(HTTPException) as exc_info: extract_user_token(headers_invalid_auth) - assert exc_info.value.status_code == 400 + assert exc_info.value.status_code == 401 assert exc_info.value.detail == "No token found in Authorization header" diff --git a/tests/unit/app/endpoints/test_query.py b/tests/unit/app/endpoints/test_query.py index 9b10d970e..2b215fa0d 100644 --- a/tests/unit/app/endpoints/test_query.py +++ b/tests/unit/app/endpoints/test_query.py @@ -459,7 +459,7 @@ def test_select_model_and_provider_id_no_available_models( mock_client.models.list(), query_request.model, query_request.provider ) - assert "No LLM model found in available models" in str(exc_info.value) + assert "No models available" in str(exc_info.value) def test_validate_attachments_metadata() -> None: diff --git a/tests/unit/authentication/test_jwk_token.py b/tests/unit/authentication/test_jwk_token.py index 4a08294fe..613c821e6 100644 --- a/tests/unit/authentication/test_jwk_token.py +++ b/tests/unit/authentication/test_jwk_token.py @@ -302,7 +302,7 @@ async def test_no_bearer( with pytest.raises(HTTPException) as exc_info: await dependency(not_bearer_token_request) - assert exc_info.value.status_code == 400 + assert exc_info.value.status_code == 401 assert exc_info.value.detail == "No token found in Authorization header" diff --git a/tests/unit/authentication/test_noop_with_token.py b/tests/unit/authentication/test_noop_with_token.py index b18ae7a16..8fcd684b8 100644 --- a/tests/unit/authentication/test_noop_with_token.py +++ b/tests/unit/authentication/test_noop_with_token.py @@ -81,7 +81,7 @@ async def test_noop_with_token_auth_dependency_no_token() -> None: with pytest.raises(HTTPException) as exc_info: await dependency(request) - assert exc_info.value.status_code == 400 + assert exc_info.value.status_code == 401 assert exc_info.value.detail == "No Authorization header found" @@ -102,5 +102,5 @@ async def test_noop_with_token_auth_dependency_no_bearer() -> None: with pytest.raises(HTTPException) as exc_info: await dependency(request) - assert exc_info.value.status_code == 400 + assert exc_info.value.status_code == 401 assert exc_info.value.detail == "No token found in Authorization header" diff --git a/tests/unit/authentication/test_utils.py b/tests/unit/authentication/test_utils.py index ee1d34dc8..5b1949ffd 100644 --- a/tests/unit/authentication/test_utils.py +++ b/tests/unit/authentication/test_utils.py @@ -19,7 +19,7 @@ def test_extract_user_token_no_header() -> None: try: extract_user_token(headers) except HTTPException as exc: - assert exc.status_code == 400 + assert exc.status_code == 401 assert exc.detail == "No Authorization header found" @@ -29,5 +29,5 @@ def test_extract_user_token_invalid_format() -> None: try: extract_user_token(headers) except HTTPException as exc: - assert exc.status_code == 400 + assert exc.status_code == 401 assert exc.detail == "No token found in Authorization header"