Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@
}
]
},
"400": {
"401": {
"description": "Missing or invalid credentials provided by client",
"content": {
"application/json": {
Expand All @@ -375,6 +375,9 @@
}
}
},
"404": {
"description": "Requested model or provider not found"
},
"429": {
"description": "The quota has been exceeded",
"content": {
Expand Down
22 changes: 18 additions & 4 deletions src/app/endpoints/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,17 @@
}
],
},
400: {
401: {
"description": "Missing or invalid credentials provided by client",
"model": UnauthorizedResponse,
},
403: {
"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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/authentication/jwk_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/authentication/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
30 changes: 30 additions & 0 deletions tests/e2e/config/no-models-run.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not valid config for lightspeed-stack.

# 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: []
2 changes: 2 additions & 0 deletions tests/e2e/features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 42 additions & 1 deletion tests/e2e/features/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,43 @@ 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 service is started locally
And REST API service prefix is /v1
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"}
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above scenario fails (returns 200) because "Bearer invalid" is actually a valid token - everything that matches "Bearer something" is valid token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you actually want to test invalid bearer token, write something like BearerInvalid, Invalid or whatever you want. When you use Bearer with anything, it always passes the check


Scenario: Check if LLM responds to sent question with error when model does not exist
Given The service is started locally
And REST API service prefix is /v1
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 service is started locally
And REST API service prefix is /v1
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
Expand Down Expand Up @@ -138,3 +168,14 @@ Scenario: Check if LLM responds for query request with error for missing query
}
"""
Then The status code of the response is 200

@no_models
Scenario: Check if LLM responds with an error when no models are configured
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"}
"""
Then The status code of the response is 404
And The body of the response contains No models available

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above scenario fails because, for the OpenAI provider, all its models are available by default even if the models list is empty. This behavior is specific to OpenAI and is unaffected by the model configuration.

2 changes: 1 addition & 1 deletion tests/e2e/features/streaming_query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_openapi_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}),
Expand Down Expand Up @@ -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"}),
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/app/endpoints/test_authorized.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion tests/unit/app/endpoints/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/authentication/test_jwk_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
4 changes: 2 additions & 2 deletions tests/unit/authentication/test_noop_with_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand All @@ -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"
4 changes: 2 additions & 2 deletions tests/unit/authentication/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand All @@ -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"
Loading