mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-07-27 06:28:50 +00:00
# What does this PR do?
Today, external providers are installed via the `external_providers_dir`
in the config. This necessitates users to understand the `ProviderSpec`
and set up their directories accordingly. This process splits up the
config for the stack across multiple files, directories, and formats.
Most (if not all) external providers today have a
[get_provider_spec](559cb18fbb/src/ramalama_stack/provider.py (L9)
)
method that sits unused. Utilizing this method rather than the
providers.d route allows for a much easier installation process for
external providers and limits the amount of extra configuration a
regular user has to do to get their stack off the ground.
To accomplish this and wire it throughout the build process, Introduce
the concept of a `module` for users to specify for an external provider
upon build time. In order to facilitate this, align the build and run
spec to use `Provider` class rather than the stringified provider_type
that build currently uses.
For example, say this is in your build config:
```
- provider_id: ramalama
provider_type: remote::ramalama
module: ramalama_stack
```
during build (in the various `build_...` scripts), additionally to
installing any pip dependencies we will also install this module and use
the `get_provider_spec` method to retrieve the ProviderSpec that is
currently specified using `providers.d`.
In production so far, providing instructions for installing external
providers for users has been difficult: they need to install the module
as a pre-req, create the providers.d directory, copy in the provider
spec, and also copy in the necessary build/run yaml files. Accessing an
external provider should be as easy as possible, and pointing to its
installable module aligns more with the rest of our build and dependency
management process.
For now, `external_providers_dir` still exists as an alternate more
declarative method of using external providers.
## Test Plan
added an integration test installing an external provider from module
and more unit test coverage for `get_provider_registry`
( the warning in yellow is expected, the module is installed inside of
the build env, not where we are running the command)
<img width="1119" height="400" alt="Screenshot 2025-07-24 at 11 30
48 AM"
src="https://github.com/user-attachments/assets/1efbaf45-b9e8-451a-bd63-264ed664706d"
/>
<img width="1154" height="618" alt="Screenshot 2025-07-24 at 11 31
14 AM"
src="https://github.com/user-attachments/assets/feb2b3ea-c5dd-418e-9662-9a3bd5dd6bdc"
/>
---------
Signed-off-by: Charlie Doern <cdoern@redhat.com>
177 lines
6.4 KiB
Python
177 lines
6.4 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the terms described in the LICENSE file in
|
|
# the root directory of this source tree.
|
|
|
|
import importlib.resources
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from pydantic import BaseModel
|
|
from termcolor import cprint
|
|
|
|
from llama_stack.distribution.datatypes import BuildConfig
|
|
from llama_stack.distribution.distribution import get_provider_registry
|
|
from llama_stack.distribution.external import load_external_apis
|
|
from llama_stack.distribution.utils.exec import run_command
|
|
from llama_stack.distribution.utils.image_types import LlamaStackImageType
|
|
from llama_stack.providers.datatypes import Api
|
|
from llama_stack.templates.template import DistributionTemplate
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# These are the dependencies needed by the distribution server.
|
|
# `llama-stack` is automatically installed by the installation script.
|
|
SERVER_DEPENDENCIES = [
|
|
"aiosqlite",
|
|
"fastapi",
|
|
"fire",
|
|
"httpx",
|
|
"uvicorn",
|
|
"opentelemetry-sdk",
|
|
"opentelemetry-exporter-otlp-proto-http",
|
|
]
|
|
|
|
|
|
class ApiInput(BaseModel):
|
|
api: Api
|
|
provider: str
|
|
|
|
|
|
def get_provider_dependencies(
|
|
config: BuildConfig | DistributionTemplate,
|
|
) -> tuple[list[str], list[str], list[str]]:
|
|
"""Get normal and special dependencies from provider configuration."""
|
|
if isinstance(config, DistributionTemplate):
|
|
config = config.build_config()
|
|
|
|
providers = config.distribution_spec.providers
|
|
additional_pip_packages = config.additional_pip_packages
|
|
|
|
deps = []
|
|
external_provider_deps = []
|
|
registry = get_provider_registry(config)
|
|
for api_str, provider_or_providers in providers.items():
|
|
providers_for_api = registry[Api(api_str)]
|
|
|
|
providers = provider_or_providers if isinstance(provider_or_providers, list) else [provider_or_providers]
|
|
|
|
for provider in providers:
|
|
# Providers from BuildConfig and RunConfig are subtly different - not great
|
|
provider_type = provider if isinstance(provider, str) else provider.provider_type
|
|
|
|
if provider_type not in providers_for_api:
|
|
raise ValueError(f"Provider `{provider}` is not available for API `{api_str}`")
|
|
|
|
provider_spec = providers_for_api[provider_type]
|
|
if hasattr(provider_spec, "is_external") and provider_spec.is_external:
|
|
# this ensures we install the top level module for our external providers
|
|
if provider_spec.module:
|
|
if isinstance(provider_spec.module, str):
|
|
external_provider_deps.append(provider_spec.module)
|
|
else:
|
|
external_provider_deps.extend(provider_spec.module)
|
|
if hasattr(provider_spec, "pip_packages"):
|
|
deps.extend(provider_spec.pip_packages)
|
|
if hasattr(provider_spec, "container_image") and provider_spec.container_image:
|
|
raise ValueError("A stack's dependencies cannot have a container image")
|
|
|
|
normal_deps = []
|
|
special_deps = []
|
|
for package in deps:
|
|
if "--no-deps" in package or "--index-url" in package:
|
|
special_deps.append(package)
|
|
else:
|
|
normal_deps.append(package)
|
|
|
|
normal_deps.extend(additional_pip_packages or [])
|
|
|
|
return list(set(normal_deps)), list(set(special_deps)), list(set(external_provider_deps))
|
|
|
|
|
|
def print_pip_install_help(config: BuildConfig):
|
|
normal_deps, special_deps = get_provider_dependencies(config)
|
|
|
|
cprint(
|
|
f"Please install needed dependencies using the following commands:\n\nuv pip install {' '.join(normal_deps)}",
|
|
color="yellow",
|
|
file=sys.stderr,
|
|
)
|
|
for special_dep in special_deps:
|
|
cprint(f"uv pip install {special_dep}", color="yellow", file=sys.stderr)
|
|
print()
|
|
|
|
|
|
def build_image(
|
|
build_config: BuildConfig,
|
|
build_file_path: Path,
|
|
image_name: str,
|
|
template_or_config: str,
|
|
run_config: str | None = None,
|
|
):
|
|
container_base = build_config.distribution_spec.container_image or "python:3.12-slim"
|
|
|
|
normal_deps, special_deps, external_provider_deps = get_provider_dependencies(build_config)
|
|
normal_deps += SERVER_DEPENDENCIES
|
|
if build_config.external_apis_dir:
|
|
external_apis = load_external_apis(build_config)
|
|
if external_apis:
|
|
for _, api_spec in external_apis.items():
|
|
normal_deps.extend(api_spec.pip_packages)
|
|
|
|
if build_config.image_type == LlamaStackImageType.CONTAINER.value:
|
|
script = str(importlib.resources.files("llama_stack") / "distribution/build_container.sh")
|
|
args = [
|
|
script,
|
|
"--template-or-config",
|
|
template_or_config,
|
|
"--image-name",
|
|
image_name,
|
|
"--container-base",
|
|
container_base,
|
|
"--normal-deps",
|
|
" ".join(normal_deps),
|
|
]
|
|
# When building from a config file (not a template), include the run config path in the
|
|
# build arguments
|
|
if run_config is not None:
|
|
args.extend(["--run-config", run_config])
|
|
elif build_config.image_type == LlamaStackImageType.CONDA.value:
|
|
script = str(importlib.resources.files("llama_stack") / "distribution/build_conda_env.sh")
|
|
args = [
|
|
script,
|
|
"--env-name",
|
|
str(image_name),
|
|
"--build-file-path",
|
|
str(build_file_path),
|
|
"--normal-deps",
|
|
" ".join(normal_deps),
|
|
]
|
|
elif build_config.image_type == LlamaStackImageType.VENV.value:
|
|
script = str(importlib.resources.files("llama_stack") / "distribution/build_venv.sh")
|
|
args = [
|
|
script,
|
|
"--env-name",
|
|
str(image_name),
|
|
"--normal-deps",
|
|
" ".join(normal_deps),
|
|
]
|
|
|
|
# Always pass both arguments, even if empty, to maintain consistent positional arguments
|
|
if special_deps:
|
|
args.extend(["--optional-deps", "#".join(special_deps)])
|
|
if external_provider_deps:
|
|
args.extend(
|
|
["--external-provider-deps", "#".join(external_provider_deps)]
|
|
) # the script will install external provider module, get its deps, and install those too.
|
|
|
|
return_code = run_command(args)
|
|
|
|
if return_code != 0:
|
|
log.error(
|
|
f"Failed to build target {image_name} with return code {return_code}",
|
|
)
|
|
|
|
return return_code
|