diff --git a/src/app/main.py b/src/app/main.py index 74a6b86a1..e4ee83905 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -8,6 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from starlette.routing import Mount, Route, WebSocketRoute +from llama_stack_client import APIConnectionError from authorization.azure_token_manager import AzureEntraIDManager import metrics @@ -50,10 +51,23 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: "Token refresh will be retried on next Azure request." ) - await AsyncLlamaStackClientHolder().load(configuration.configuration.llama_stack) + llama_stack_config = configuration.configuration.llama_stack + await AsyncLlamaStackClientHolder().load(llama_stack_config) client = AsyncLlamaStackClientHolder().get_client() # check if the Llama Stack version is supported by the service - await check_llama_stack_version(client) + try: + await check_llama_stack_version(client) + except APIConnectionError as e: + llama_stack_url = llama_stack_config.url + logger.error( + "Failed to connect to Llama Stack at '%s'. " + "Please verify that the 'llama_stack.url' configuration is correct " + "and that the Llama Stack service is running and accessible. " + "Original error: %s", + llama_stack_url, + e, + ) + raise logger.info("Registering MCP servers") await register_mcp_servers_async(logger, configuration.configuration) diff --git a/src/client.py b/src/client.py index bd48acf39..6f1646910 100644 --- a/src/client.py +++ b/src/client.py @@ -70,8 +70,10 @@ def _load_service_client(self, config: LlamaStackConfiguration) -> None: "Using timeout of %d seconds for Llama Stack requests", config.timeout ) api_key = config.api_key.get_secret_value() if config.api_key else None + # Convert AnyHttpUrl to string for the client + base_url = str(config.url) if config.url else None self._lsc = AsyncLlamaStackClient( - base_url=config.url, api_key=api_key, timeout=config.timeout + base_url=base_url, api_key=api_key, timeout=config.timeout ) def _enrich_library_config( diff --git a/src/models/config.py b/src/models/config.py index b74a0233d..d038ebc1f 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -533,10 +533,11 @@ class LlamaStackConfiguration(ConfigurationBase): - [Build AI Applications with Llama Stack](https://llamastack.github.io/) """ - url: Optional[str] = Field( + url: Optional[AnyHttpUrl] = Field( None, title="Llama Stack URL", - description="URL to Llama Stack service; used when library mode is disabled", + description="URL to Llama Stack service; used when library mode is disabled. " + "Must be a valid HTTP or HTTPS URL.", ) api_key: Optional[SecretStr] = Field( diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 294042a51..60e6d09a6 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -69,7 +69,7 @@ def test_loading_proper_configuration(configuration_filename: str) -> None: # check 'llama_stack' section ls_config = cfg.llama_stack_configuration assert ls_config.use_as_library_client is False - assert ls_config.url == "http://localhost:8321" + assert str(ls_config.url) == "http://localhost:8321/" assert ls_config.api_key is not None assert ls_config.api_key.get_secret_value() == "xyzzy" diff --git a/tests/unit/models/config/test_authentication_configuration.py b/tests/unit/models/config/test_authentication_configuration.py index 5e99f3aa2..7575751a0 100644 --- a/tests/unit/models/config/test_authentication_configuration.py +++ b/tests/unit/models/config/test_authentication_configuration.py @@ -307,7 +307,7 @@ def test_authentication_configuration_in_config_noop() -> None: llama_stack=LlamaStackConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", - url="localhost", + url="http://localhost", api_key=SecretStr(""), timeout=60, ), @@ -346,7 +346,7 @@ def test_authentication_configuration_skip_readiness_probe() -> None: llama_stack=LlamaStackConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", - url="localhost", + url="http://localhost", api_key=SecretStr(""), timeout=60, ), @@ -393,7 +393,7 @@ def test_authentication_configuration_in_config_k8s() -> None: llama_stack=LlamaStackConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", - url="localhost", + url="http://localhost", api_key=SecretStr(""), timeout=60, ), @@ -450,7 +450,7 @@ def test_authentication_configuration_in_config_rh_identity() -> None: llama_stack=LlamaStackConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", - url="localhost", + url="http://localhost", api_key=SecretStr(""), timeout=60, ), @@ -497,7 +497,7 @@ def test_authentication_configuration_in_config_jwktoken() -> None: llama_stack=LlamaStackConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", - url="localhost", + url="http://localhost", api_key=SecretStr(""), timeout=60, ), diff --git a/tests/unit/models/config/test_llama_stack_configuration.py b/tests/unit/models/config/test_llama_stack_configuration.py index 4d8465b4c..cc2db8236 100644 --- a/tests/unit/models/config/test_llama_stack_configuration.py +++ b/tests/unit/models/config/test_llama_stack_configuration.py @@ -1,6 +1,7 @@ """Unit tests for LlamaStackConfiguration model.""" import pytest +from pydantic import ValidationError from utils.checks import InvalidConfigurationError @@ -89,3 +90,37 @@ def test_llama_stack_wrong_configuration_no_config_file() -> None: LlamaStackConfiguration( use_as_library_client=True ) # pyright: ignore[reportCallIssue] + + +def test_llama_stack_configuration_valid_http_url() -> None: + """Test that valid HTTP URLs are accepted.""" + config = LlamaStackConfiguration( + url="http://localhost:8321" + ) # pyright: ignore[reportCallIssue] + assert config is not None + assert str(config.url) == "http://localhost:8321/" + + +def test_llama_stack_configuration_valid_https_url() -> None: + """Test that valid HTTPS URLs are accepted.""" + config = LlamaStackConfiguration( + url="https://llama-stack.example.com:8321" + ) # pyright: ignore[reportCallIssue] + assert config is not None + assert str(config.url) == "https://llama-stack.example.com:8321/" + + +def test_llama_stack_configuration_malformed_url_rejected() -> None: + """Test that malformed URLs are rejected with a ValidationError.""" + with pytest.raises(ValidationError, match="Input should be a valid URL"): + LlamaStackConfiguration( + url="not-a-valid-url" + ) # pyright: ignore[reportCallIssue] + + +def test_llama_stack_configuration_invalid_scheme_rejected() -> None: + """Test that URLs without http/https scheme are rejected.""" + with pytest.raises(ValidationError, match="URL scheme should be 'http' or 'https'"): + LlamaStackConfiguration( + url="ftp://localhost:8321" + ) # pyright: ignore[reportCallIssue] diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 3edd3999a..2cf0d56de 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -142,7 +142,7 @@ def test_init_from_dict() -> None: # check for llama_stack_configuration subsection assert cfg.llama_stack_configuration.api_key is not None assert cfg.llama_stack_configuration.api_key.get_secret_value() == "xyzzy" - assert cfg.llama_stack_configuration.url == "http://x.y.com:1234" + assert str(cfg.llama_stack_configuration.url) == "http://x.y.com:1234/" assert cfg.llama_stack_configuration.use_as_library_client is False # check for service_configuration subsection