feat: load config class when doing variable substitution

When using bash style substitution env variable in distribution
template, we are processing the string and convert it to the type
associated with the provider's config class. This allows us to return
the proper type. This is crucial for api key since they are not strings
anymore but SecretStr. If the key is unset we will get an empty string
which will result in a Pydantic error like:

```
ERROR    2025-09-25 21:40:44,565 __main__:527 core::server: Error creating app: 1 validation error for AnthropicConfig
         api_key
           Input should be a valid string
             For further information visit
             https://errors.pydantic.dev/2.11/v/string_type
```

Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
Sébastien Han 2025-09-25 10:27:41 +02:00
parent 4af141292f
commit bc64635835
No known key found for this signature in database
79 changed files with 381 additions and 216 deletions

View file

@ -14,7 +14,7 @@ NVIDIA's dataset I/O provider for accessing datasets from NVIDIA's data platform
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `str \| None` | No | | The NVIDIA API key. |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The NVIDIA API key. |
| `dataset_namespace` | `str \| None` | No | default | The NVIDIA dataset namespace. |
| `project_id` | `str \| None` | No | test-project | The NVIDIA project ID. |
| `datasets_url` | `<class 'str'>` | No | http://nemo.test | Base URL for the NeMo Dataset API |

View file

@ -17,7 +17,7 @@ AWS S3-based file storage provider for scalable cloud file management with metad
| `bucket_name` | `<class 'str'>` | No | | S3 bucket name to store files |
| `region` | `<class 'str'>` | No | us-east-1 | AWS region where the bucket is located |
| `aws_access_key_id` | `str \| None` | No | | AWS access key ID (optional if using IAM roles) |
| `aws_secret_access_key` | `<class 'pydantic.types.SecretStr'>` | No | | AWS secret access key (optional if using IAM roles) |
| `aws_secret_access_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | AWS secret access key (optional if using IAM roles) |
| `endpoint_url` | `str \| None` | No | | Custom S3 endpoint URL (for MinIO, LocalStack, etc.) |
| `auto_create_bucket` | `<class 'bool'>` | No | False | Automatically create the S3 bucket if it doesn't exist |
| `metadata_store` | `utils.sqlstore.sqlstore.SqliteSqlStoreConfig \| utils.sqlstore.sqlstore.PostgresSqlStoreConfig` | No | sqlite | SQL store configuration for file metadata |

View file

@ -14,7 +14,7 @@ Anthropic inference provider for accessing Claude models and Anthropic's AI serv
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | API key for Anthropic models |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | API key for Anthropic models |
## Sample Configuration

View file

@ -21,7 +21,7 @@ https://learn.microsoft.com/en-us/azure/ai-foundry/openai/overview
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | Azure API key for Azure |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | Azure API key for Azure |
| `api_base` | `<class 'pydantic.networks.HttpUrl'>` | No | | Azure API base for Azure (e.g., https://your-resource-name.openai.azure.com) |
| `api_version` | `str \| None` | No | | Azure API version for Azure (e.g., 2024-12-01-preview) |
| `api_type` | `str \| None` | No | azure | Azure API type for Azure (e.g., azure) |

View file

@ -15,8 +15,8 @@ AWS Bedrock inference provider for accessing various AI models through AWS's man
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `aws_access_key_id` | `str \| None` | No | | The AWS access key to use. Default use environment variable: AWS_ACCESS_KEY_ID |
| `aws_secret_access_key` | `<class 'pydantic.types.SecretStr'>` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY |
| `aws_session_token` | `<class 'pydantic.types.SecretStr'>` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN |
| `aws_secret_access_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY |
| `aws_session_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN |
| `region_name` | `str \| None` | No | | The default AWS Region to use, for example, us-west-1 or us-west-2.Default use environment variable: AWS_DEFAULT_REGION |
| `profile_name` | `str \| None` | No | | The profile name that contains credentials to use.Default use environment variable: AWS_PROFILE |
| `total_max_attempts` | `int \| None` | No | | An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. Default use environment variable: AWS_MAX_ATTEMPTS |

View file

@ -15,7 +15,7 @@ Cerebras inference provider for running models on Cerebras Cloud platform.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `base_url` | `<class 'str'>` | No | https://api.cerebras.ai | Base URL for the Cerebras API |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | Cerebras API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | Cerebras API Key |
## Sample Configuration

View file

@ -15,7 +15,7 @@ Databricks inference provider for running models on Databricks' unified analytic
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | | The URL for the Databricks model serving endpoint |
| `api_token` | `<class 'pydantic.types.SecretStr'>` | No | | The Databricks API token |
| `api_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Databricks API token |
## Sample Configuration

View file

@ -16,7 +16,7 @@ Fireworks AI inference provider for Llama models and other AI models on the Fire
|-------|------|----------|---------|-------------|
| `allowed_models` | `list[str \| None` | No | | List of models that should be registered with the model registry. If None, all models are allowed. |
| `url` | `<class 'str'>` | No | https://api.fireworks.ai/inference/v1 | The URL for the Fireworks server |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Fireworks.ai API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Fireworks.ai API Key |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Google Gemini inference provider for accessing Gemini models and Google's AI ser
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | API key for Gemini models |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | API key for Gemini models |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Groq inference provider for ultra-fast inference using Groq's LPU technology.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Groq API key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Groq API key |
| `url` | `<class 'str'>` | No | https://api.groq.com | The URL for the Groq AI server |
## Sample Configuration

View file

@ -15,7 +15,7 @@ HuggingFace Inference Endpoints provider for dedicated model serving.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `endpoint_name` | `<class 'str'>` | No | | The name of the Hugging Face Inference Endpoint in the format of '&#123;namespace&#125;/&#123;endpoint_name&#125;' (e.g. 'my-cool-org/meta-llama-3-1-8b-instruct-rce'). Namespace is optional and will default to the user account if not provided. |
| `api_token` | `pydantic.types.SecretStr \| None` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) |
| `api_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) |
## Sample Configuration

View file

@ -15,7 +15,7 @@ HuggingFace Inference API serverless provider for on-demand model inference.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `huggingface_repo` | `<class 'str'>` | No | | The model ID of the model on the Hugging Face Hub (e.g. 'meta-llama/Meta-Llama-3.1-70B-Instruct') |
| `api_token` | `pydantic.types.SecretStr \| None` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) |
| `api_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Llama OpenAI-compatible provider for using Llama models with OpenAI API format.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Llama API key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Llama API key |
| `openai_compat_api_base` | `<class 'str'>` | No | https://api.llama.com/compat/v1/ | The URL for the Llama API server |
## Sample Configuration

View file

@ -15,7 +15,7 @@ NVIDIA inference provider for accessing NVIDIA NIM models and AI services.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | https://integrate.api.nvidia.com | A base url for accessing the NVIDIA NIM |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The NVIDIA API key, only needed of using the hosted service |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The NVIDIA API key, only needed of using the hosted service |
| `timeout` | `<class 'int'>` | No | 60 | Timeout for the HTTP requests |
| `append_api_version` | `<class 'bool'>` | No | True | When set to false, the API version will not be appended to the base_url. By default, it is true. |

View file

@ -14,7 +14,7 @@ OpenAI inference provider for accessing GPT models and other OpenAI services.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | API key for OpenAI models |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | API key for OpenAI models |
| `base_url` | `<class 'str'>` | No | https://api.openai.com/v1 | Base URL for OpenAI API |
## Sample Configuration

View file

@ -15,7 +15,7 @@ Passthrough inference provider for connecting to any external inference service
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | | The URL for the passthrough endpoint |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | API Key for the passthrouth endpoint |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | API Key for the passthrouth endpoint |
## Sample Configuration

View file

@ -15,7 +15,7 @@ RunPod inference provider for running models on RunPod's cloud GPU platform.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `str \| None` | No | | The URL for the Runpod model serving endpoint |
| `api_token` | `<class 'pydantic.types.SecretStr'>` | No | | The API token |
| `api_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The API token |
## Sample Configuration

View file

@ -15,7 +15,7 @@ SambaNova inference provider for running models on SambaNova's dataflow architec
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | https://api.sambanova.ai/v1 | The URL for the SambaNova AI server |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The SambaNova cloud API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The SambaNova cloud API Key |
## Sample Configuration

View file

@ -16,7 +16,7 @@ Together AI inference provider for open-source models and collaborative AI devel
|-------|------|----------|---------|-------------|
| `allowed_models` | `list[str \| None` | No | | List of models that should be registered with the model registry. If None, all models are allowed. |
| `url` | `<class 'str'>` | No | https://api.together.xyz/v1 | The URL for the Together AI server |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Together AI API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Together AI API Key |
## Sample Configuration

View file

@ -16,7 +16,7 @@ Remote vLLM inference provider for connecting to vLLM servers.
|-------|------|----------|---------|-------------|
| `url` | `str \| None` | No | | The URL for the vLLM model serving endpoint |
| `max_tokens` | `<class 'int'>` | No | 4096 | Maximum number of tokens to generate. |
| `api_token` | `<class 'pydantic.types.SecretStr'>` | No | | The API token |
| `api_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The API token |
| `tls_verify` | `bool \| str` | No | True | Whether to verify TLS certificates. Can be a boolean or a path to a CA certificate file. |
| `refresh_models` | `<class 'bool'>` | No | False | Whether to refresh models periodically |

View file

@ -15,7 +15,7 @@ IBM WatsonX inference provider for accessing AI models on IBM's WatsonX platform
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | https://us-south.ml.cloud.ibm.com | A base url for accessing the watsonx.ai |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The watsonx API key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The watsonx API key |
| `project_id` | `str \| None` | No | | The Project ID key |
| `timeout` | `<class 'int'>` | No | 60 | Timeout for the HTTP requests |

View file

@ -14,7 +14,7 @@ NVIDIA's post-training provider for fine-tuning models on NVIDIA's platform.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `str \| None` | No | | The NVIDIA API key. |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The NVIDIA API key. |
| `dataset_namespace` | `str \| None` | No | default | The NVIDIA dataset namespace. |
| `project_id` | `str \| None` | No | test-example-model@v1 | The NVIDIA project ID. |
| `customizer_url` | `str \| None` | No | | Base URL for the NeMo Customizer API |

View file

@ -15,8 +15,8 @@ AWS Bedrock safety provider for content moderation using AWS's safety services.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `aws_access_key_id` | `str \| None` | No | | The AWS access key to use. Default use environment variable: AWS_ACCESS_KEY_ID |
| `aws_secret_access_key` | `<class 'pydantic.types.SecretStr'>` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY |
| `aws_session_token` | `<class 'pydantic.types.SecretStr'>` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN |
| `aws_secret_access_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY |
| `aws_session_token` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN |
| `region_name` | `str \| None` | No | | The default AWS Region to use, for example, us-west-1 or us-west-2.Default use environment variable: AWS_DEFAULT_REGION |
| `profile_name` | `str \| None` | No | | The profile name that contains credentials to use.Default use environment variable: AWS_PROFILE |
| `total_max_attempts` | `int \| None` | No | | An integer representing the maximum number of attempts that will be made for a single request, including the initial attempt. Default use environment variable: AWS_MAX_ATTEMPTS |

View file

@ -15,7 +15,7 @@ SambaNova's safety provider for content moderation and safety filtering.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `url` | `<class 'str'>` | No | https://api.sambanova.ai/v1 | The URL for the SambaNova AI server |
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The SambaNova cloud API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The SambaNova cloud API Key |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Braintrust scoring provider for evaluation and scoring using the Braintrust plat
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `openai_api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The OpenAI API Key |
| `openai_api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The OpenAI API Key |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Bing Search tool for web search capabilities using Microsoft's search engine.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Bing API key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Bing API key |
| `top_k` | `<class 'int'>` | No | 3 | |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Brave Search tool for web search capabilities with privacy-focused results.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Brave Search API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Brave Search API Key |
| `max_results` | `<class 'int'>` | No | 3 | The maximum number of results to return |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Tavily Search tool for AI-optimized web search with structured results.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `<class 'pydantic.types.SecretStr'>` | No | | The Tavily Search API Key |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The Tavily Search API Key |
| `max_results` | `<class 'int'>` | No | 3 | The maximum number of results to return |
## Sample Configuration

View file

@ -14,7 +14,7 @@ Wolfram Alpha tool for computational knowledge and mathematical calculations.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_key` | `str \| None` | No | | |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The WolframAlpha API Key |
## Sample Configuration

View file

@ -217,7 +217,7 @@ See [PGVector's documentation](https://github.com/pgvector/pgvector) for more de
| `port` | `int \| None` | No | 5432 | |
| `db` | `str \| None` | No | postgres | |
| `user` | `str \| None` | No | postgres | |
| `password` | `<class 'pydantic.types.SecretStr'>` | No | ********** | |
| `password` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | ********** | |
| `kvstore` | `utils.kvstore.config.RedisKVStoreConfig \| utils.kvstore.config.SqliteKVStoreConfig \| utils.kvstore.config.PostgresKVStoreConfig \| utils.kvstore.config.MongoDBKVStoreConfig, annotation=NoneType, required=False, default='sqlite', discriminator='type'` | No | | Config for KV store backend (SQLite only for now) |
## Sample Configuration

View file

@ -22,7 +22,7 @@ Please refer to the inline provider documentation.
| `grpc_port` | `<class 'int'>` | No | 6334 | |
| `prefer_grpc` | `<class 'bool'>` | No | False | |
| `https` | `bool \| None` | No | | |
| `api_key` | `str \| None` | No | | |
| `api_key` | `<class 'llama_stack.core.secret_types.MySecretStr'>` | No | | The API key for the Qdrant instance |
| `prefix` | `str \| None` | No | | |
| `timeout` | `int \| None` | No | | |
| `host` | `str \| None` | No | | |

View file

@ -216,7 +216,7 @@ def run_stack_build_command(args: argparse.Namespace) -> None:
with open(args.config) as f:
try:
contents = yaml.safe_load(f)
contents = replace_env_vars(contents)
contents = replace_env_vars(contents, provider_registry=get_provider_registry())
build_config = BuildConfig(**contents)
if args.image_type:
build_config.image_type = args.image_type

View file

@ -165,7 +165,7 @@ def upgrade_from_routing_table(
def parse_and_maybe_upgrade_config(config_dict: dict[str, Any]) -> StackRunConfig:
version = config_dict.get("version", None)
if version == LLAMA_STACK_RUN_CONFIG_VERSION:
processed_config_dict = replace_env_vars(config_dict)
processed_config_dict = replace_env_vars(config_dict, provider_registry=get_provider_registry())
return StackRunConfig(**cast_image_name_to_string(processed_config_dict))
if "routing_table" in config_dict:
@ -177,5 +177,5 @@ def parse_and_maybe_upgrade_config(config_dict: dict[str, Any]) -> StackRunConfi
if not config_dict.get("external_providers_dir", None):
config_dict["external_providers_dir"] = EXTERNAL_PROVIDERS_DIR
processed_config_dict = replace_env_vars(config_dict)
processed_config_dict = replace_env_vars(config_dict, provider_registry=get_provider_registry())
return StackRunConfig(**cast_image_name_to_string(processed_config_dict))

View file

@ -33,6 +33,7 @@ from termcolor import cprint
from llama_stack.core.build import print_pip_install_help
from llama_stack.core.configure import parse_and_maybe_upgrade_config
from llama_stack.core.datatypes import Api, BuildConfig, BuildProvider, DistributionSpec
from llama_stack.core.distribution import get_provider_registry
from llama_stack.core.request_headers import (
PROVIDER_DATA_VAR,
request_provider_data_context,
@ -220,7 +221,9 @@ class AsyncLlamaStackAsLibraryClient(AsyncLlamaStackClient):
config_path = Path(config_path_or_distro_name)
if not config_path.exists():
raise ValueError(f"Config file {config_path} does not exist")
config_dict = replace_env_vars(yaml.safe_load(config_path.read_text()))
config_dict = replace_env_vars(
yaml.safe_load(config_path.read_text()), provider_registry=get_provider_registry()
)
config = parse_and_maybe_upgrade_config(config_dict)
else:
# distribution

View file

@ -0,0 +1,21 @@
# 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.
from pydantic.types import SecretStr
class MySecretStr(SecretStr):
"""A SecretStr that can accept None values to avoid mypy type errors.
This is useful for optional secret fields where you want to avoid
explicit None checks in consuming code.
We chose to not use the SecretStr from pydantic because it does not allow None values and will
let the provider's library fail if the secret is not provided.
"""
def __init__(self, secret_value: str | None = None) -> None:
SecretStr.__init__(self, secret_value) # type: ignore[arg-type]

View file

@ -43,7 +43,7 @@ from llama_stack.core.datatypes import (
StackRunConfig,
process_cors_config,
)
from llama_stack.core.distribution import builtin_automatically_routed_apis
from llama_stack.core.distribution import builtin_automatically_routed_apis, get_provider_registry
from llama_stack.core.external import load_external_apis
from llama_stack.core.request_headers import (
PROVIDER_DATA_VAR,
@ -371,7 +371,7 @@ def create_app(
logger.error(f"Error: {str(e)}")
raise ValueError(f"Invalid environment variable format: {env_pair}") from e
config = replace_env_vars(config_contents)
config = replace_env_vars(config_contents, provider_registry=get_provider_registry())
config = StackRunConfig(**cast_image_name_to_string(config))
_log_run_config(run_config=config)
@ -524,7 +524,10 @@ def main(args: argparse.Namespace | None = None):
env_vars=args.env,
)
except Exception as e:
import traceback
logger.error(f"Error creating app: {str(e)}")
logger.error(f"Stack trace:\n{traceback.format_exc()}")
sys.exit(1)
config_file = resolve_config_or_distro(config_or_distro, Mode.RUN)
@ -534,7 +537,9 @@ def main(args: argparse.Namespace | None = None):
logger_config = LoggingConfig(**cfg)
else:
logger_config = None
config = StackRunConfig(**cast_image_name_to_string(replace_env_vars(config_contents)))
config = StackRunConfig(
**cast_image_name_to_string(replace_env_vars(config_contents, provider_registry=get_provider_registry()))
)
import uvicorn

View file

@ -141,12 +141,19 @@ class EnvVarError(Exception):
)
def replace_env_vars(config: Any, path: str = "") -> Any:
def replace_env_vars(
config: Any,
path: str = "",
provider_registry: dict[Api, dict[str, Any]] | None = None,
current_provider_context: dict[str, Any] | None = None,
) -> Any:
if isinstance(config, dict):
result = {}
for k, v in config.items():
try:
result[k] = replace_env_vars(v, f"{path}.{k}" if path else k)
result[k] = replace_env_vars(
v, f"{path}.{k}" if path else k, provider_registry, current_provider_context
)
except EnvVarError as e:
raise EnvVarError(e.var_name, e.path) from None
return result
@ -159,7 +166,9 @@ def replace_env_vars(config: Any, path: str = "") -> Any:
# is disabled so that we can skip config env variable expansion and avoid validation errors
if isinstance(v, dict) and "provider_id" in v:
try:
resolved_provider_id = replace_env_vars(v["provider_id"], f"{path}[{i}].provider_id")
resolved_provider_id = replace_env_vars(
v["provider_id"], f"{path}[{i}].provider_id", provider_registry, current_provider_context
)
if resolved_provider_id == "__disabled__":
logger.debug(
f"Skipping config env variable expansion for disabled provider: {v.get('provider_id', '')}"
@ -167,13 +176,19 @@ def replace_env_vars(config: Any, path: str = "") -> Any:
# Create a copy with resolved provider_id but original config
disabled_provider = v.copy()
disabled_provider["provider_id"] = resolved_provider_id
result.append(disabled_provider)
continue
except EnvVarError:
# If we can't resolve the provider_id, continue with normal processing
pass
# Set up provider context for config processing
provider_context = current_provider_context
if isinstance(v, dict) and "provider_id" in v and "provider_type" in v and provider_registry:
provider_context = _get_provider_context(v, provider_registry)
# Normal processing for non-disabled providers
result.append(replace_env_vars(v, f"{path}[{i}]"))
result.append(replace_env_vars(v, f"{path}[{i}]", provider_registry, provider_context))
except EnvVarError as e:
raise EnvVarError(e.var_name, e.path) from None
return result
@ -228,7 +243,7 @@ def replace_env_vars(config: Any, path: str = "") -> Any:
result = re.sub(pattern, get_env_var, config)
# Only apply type conversion if substitution actually happened
if result != config:
return _convert_string_to_proper_type(result)
return _convert_string_to_proper_type_with_config(result, path, current_provider_context)
return result
except EnvVarError as e:
raise EnvVarError(e.var_name, e.path) from None
@ -236,12 +251,107 @@ def replace_env_vars(config: Any, path: str = "") -> Any:
return config
def _get_provider_context(
provider_dict: dict[str, Any], provider_registry: dict[Api, dict[str, Any]]
) -> dict[str, Any] | None:
"""Get provider context information including config class for type conversion."""
try:
provider_type = provider_dict.get("provider_type")
if not provider_type:
return None
for api, providers in provider_registry.items():
if provider_type in providers:
provider_spec = providers[provider_type]
config_class = instantiate_class_type(provider_spec.config_class)
return {
"api": api,
"provider_type": provider_type,
"config_class": config_class,
"provider_spec": provider_spec,
}
except Exception as e:
logger.debug(f"Failed to get provider context: {e}")
return None
def _convert_string_to_proper_type_with_config(value: str, path: str, provider_context: dict[str, Any] | None) -> Any:
"""Convert string to proper type using provider config class field information."""
if not provider_context or not provider_context.get("config_class"):
# best effort conversion if we don't have the config class
return _convert_string_to_proper_type(value)
try:
# Extract field name from path (e.g., "providers.inference[0].config.api_key" -> "api_key")
field_name = path.split(".")[-1] if "." in path else path
config_class = provider_context["config_class"]
if hasattr(config_class, "model_fields") and field_name in config_class.model_fields:
field_info = config_class.model_fields[field_name]
field_type = field_info.annotation
return _convert_value_by_field_type(value, field_type)
else:
return _convert_string_to_proper_type(value)
except Exception as e:
logger.debug(f"Failed to convert using config class: {e}")
return _convert_string_to_proper_type(value)
def _convert_value_by_field_type(value: str, field_type: Any) -> Any:
"""Convert string value based on Pydantic field type annotation."""
import typing
from typing import get_args, get_origin
if value == "":
if field_type is None or (hasattr(typing, "get_origin") and get_origin(field_type) is type(None)):
return None
if hasattr(typing, "get_origin") and get_origin(field_type) is typing.Union:
args = get_args(field_type)
if type(None) in args:
return None
return ""
if field_type is bool or (hasattr(typing, "get_origin") and get_origin(field_type) is bool):
lowered = value.lower()
if lowered == "true":
return True
elif lowered == "false":
return False
else:
return value
if field_type is int or (hasattr(typing, "get_origin") and get_origin(field_type) is int):
try:
return int(value)
except ValueError:
return value
if field_type is float or (hasattr(typing, "get_origin") and get_origin(field_type) is float):
try:
return float(value)
except ValueError:
return value
if hasattr(typing, "get_origin") and get_origin(field_type) is typing.Union:
args = get_args(field_type)
# Try to convert to the first non-None type
for arg in args:
if arg is not type(None):
try:
return _convert_value_by_field_type(value, arg)
except Exception:
continue
return value
def _convert_string_to_proper_type(value: str) -> Any:
# This might be tricky depending on what the config type is, if 'str | None' we are
# good, if 'str' we need to keep the empty string... 'str | None' is more common and
# providers config should be typed this way.
# TODO: we could try to load the config class and see if the config has a field with type 'str | None'
# and then convert the empty string to None or not
# Fallback function for when provider config class is not available
# The main type conversion logic is now in _convert_string_to_proper_type_with_config
if value == "":
return None
@ -416,7 +526,7 @@ def get_stack_run_config_from_distro(distro: str) -> StackRunConfig:
raise ValueError(f"Distribution '{distro}' not found at {distro_path}")
run_config = yaml.safe_load(path.open())
return StackRunConfig(**replace_env_vars(run_config))
return StackRunConfig(**replace_env_vars(run_config, provider_registry=get_provider_registry()))
def run_config_from_adhoc_config_spec(
@ -452,7 +562,9 @@ def run_config_from_adhoc_config_spec(
# call method "sample_run_config" on the provider spec config class
provider_config_type = instantiate_class_type(provider_spec.config_class)
provider_config = replace_env_vars(provider_config_type.sample_run_config(__distro_dir__=distro_dir))
provider_config = replace_env_vars(
provider_config_type.sample_run_config(__distro_dir__=distro_dir), provider_registry=provider_registry
)
provider_configs_by_api[api_str] = [
Provider(

View file

@ -5,15 +5,16 @@
# the root directory of this source tree.
from typing import Any
from pydantic import BaseModel, SecretStr
from pydantic import BaseModel
from llama_stack.core.datatypes import Api
from llama_stack.core.secret_types import MySecretStr
from .config import BraintrustScoringConfig
class BraintrustProviderDataValidator(BaseModel):
openai_api_key: SecretStr
openai_api_key: MySecretStr
async def get_provider_impl(

View file

@ -17,7 +17,7 @@ from autoevals.ragas import (
ContextRelevancy,
Faithfulness,
)
from pydantic import BaseModel, SecretStr
from pydantic import BaseModel
from llama_stack.apis.datasetio import DatasetIO
from llama_stack.apis.datasets import Datasets
@ -31,6 +31,7 @@ from llama_stack.apis.scoring import (
from llama_stack.apis.scoring_functions import ScoringFn, ScoringFnParams
from llama_stack.core.datatypes import Api
from llama_stack.core.request_headers import NeedsRequestProviderData
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.datatypes import ScoringFunctionsProtocolPrivate
from llama_stack.providers.utils.common.data_schema_validator import (
get_valid_schemas,
@ -152,7 +153,7 @@ class BraintrustScoringImpl(
raise ValueError(
'Pass OpenAI API Key in the header X-LlamaStack-Provider-Data as { "openai_api_key": <your api key>}'
)
self.config.openai_api_key = SecretStr(provider_data.openai_api_key)
self.config.openai_api_key = MySecretStr(provider_data.openai_api_key)
os.environ["OPENAI_API_KEY"] = self.config.openai_api_key.get_secret_value()

View file

@ -5,12 +5,13 @@
# the root directory of this source tree.
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class BraintrustScoringConfig(BaseModel):
openai_api_key: SecretStr = Field(
default=SecretStr(""),
openai_api_key: MySecretStr = Field(
description="The OpenAI API Key",
)

View file

@ -65,8 +65,7 @@ class ConsoleSpanProcessor(SpanProcessor):
if key.startswith("__") or key in ["message", "severity"]:
continue
str_value = str(value)
logger.info(f"[dim]{key}[/dim]: {str_value}")
logger.info(f"[dim]{key}[/dim]: {value}")
def shutdown(self) -> None:
"""Shutdown the processor."""

View file

@ -10,12 +10,14 @@ from typing import Any
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class NvidiaDatasetIOConfig(BaseModel):
"""Configuration for NVIDIA DatasetIO implementation."""
api_key: str | None = Field(
default_factory=lambda: os.getenv("NVIDIA_API_KEY"),
api_key: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("NVIDIA_API_KEY", "")),
description="The NVIDIA API key.",
)

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.sqlstore.sqlstore import SqliteSqlStoreConfig, SqlStoreConfig
@ -17,9 +18,7 @@ class S3FilesImplConfig(BaseModel):
bucket_name: str = Field(description="S3 bucket name to store files")
region: str = Field(default="us-east-1", description="AWS region where the bucket is located")
aws_access_key_id: str | None = Field(default=None, description="AWS access key ID (optional if using IAM roles)")
aws_secret_access_key: SecretStr = Field(
default=SecretStr(""), description="AWS secret access key (optional if using IAM roles)"
)
aws_secret_access_key: MySecretStr = Field(description="AWS secret access key (optional if using IAM roles)")
endpoint_url: str | None = Field(default=None, description="Custom S3 endpoint URL (for MinIO, LocalStack, etc.)")
auto_create_bucket: bool = Field(
default=False, description="Automatically create the S3 bucket if it doesn't exist"

View file

@ -6,22 +6,21 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class AnthropicProviderDataValidator(BaseModel):
anthropic_api_key: SecretStr = Field(
default=SecretStr(""),
anthropic_api_key: MySecretStr = Field(
description="API key for Anthropic models",
)
@json_schema_type
class AnthropicConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="API key for Anthropic models",
)

View file

@ -7,13 +7,14 @@
import os
from typing import Any
from pydantic import BaseModel, Field, HttpUrl, SecretStr
from pydantic import BaseModel, Field, HttpUrl
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class AzureProviderDataValidator(BaseModel):
azure_api_key: SecretStr = Field(
azure_api_key: MySecretStr = Field(
description="Azure API key for Azure",
)
azure_api_base: HttpUrl = Field(
@ -31,7 +32,7 @@ class AzureProviderDataValidator(BaseModel):
@json_schema_type
class AzureConfig(BaseModel):
api_key: SecretStr = Field(
api_key: MySecretStr = Field(
description="Azure API key for Azure",
)
api_base: HttpUrl = Field(

View file

@ -7,8 +7,9 @@
import os
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
DEFAULT_BASE_URL = "https://api.cerebras.ai"
@ -20,12 +21,8 @@ class CerebrasImplConfig(BaseModel):
default=os.environ.get("CEREBRAS_BASE_URL", DEFAULT_BASE_URL),
description="Base URL for the Cerebras API",
)
api_key: SecretStr = Field(
<<<<<<< HEAD
default=SecretStr(os.environ.get("CEREBRAS_API_KEY")),
=======
default=SecretStr(os.environ.get("CEREBRAS_API_KEY", "")),
>>>>>>> a48f2009 (chore: use empty SecretStr values as default)
api_key: MySecretStr = Field(
default=MySecretStr(os.environ.get("CEREBRAS_API_KEY")),
description="Cerebras API Key",
)

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -17,8 +18,7 @@ class DatabricksImplConfig(BaseModel):
default=None,
description="The URL for the Databricks model serving endpoint",
)
api_token: SecretStr = Field(
default=SecretStr(None),
api_token: MySecretStr = Field(
description="The Databricks API token",
)

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import Field, SecretStr
from pydantic import Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.inference.model_registry import RemoteInferenceProviderConfig
from llama_stack.schema_utils import json_schema_type
@ -18,8 +19,7 @@ class FireworksImplConfig(RemoteInferenceProviderConfig):
default="https://api.fireworks.ai/inference/v1",
description="The URL for the Fireworks server",
)
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Fireworks.ai API Key",
)

View file

@ -6,22 +6,21 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class GeminiProviderDataValidator(BaseModel):
gemini_api_key: SecretStr = Field(
default=SecretStr(""),
gemini_api_key: MySecretStr = Field(
description="API key for Gemini models",
)
@json_schema_type
class GeminiConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="API key for Gemini models",
)

View file

@ -6,23 +6,22 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class GroqProviderDataValidator(BaseModel):
groq_api_key: SecretStr = Field(
default=SecretStr(""),
groq_api_key: MySecretStr = Field(
description="API key for Groq models",
)
@json_schema_type
class GroqConfig(BaseModel):
api_key: SecretStr = Field(
api_key: MySecretStr = Field(
# The Groq client library loads the GROQ_API_KEY environment variable by default
default=SecretStr(""),
description="The Groq API key",
)

View file

@ -6,22 +6,21 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class LlamaProviderDataValidator(BaseModel):
llama_api_key: SecretStr = Field(
default=SecretStr(""),
llama_api_key: MySecretStr = Field(
description="API key for api.llama models",
)
@json_schema_type
class LlamaCompatConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Llama API key",
)

View file

@ -7,8 +7,9 @@
import os
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -39,8 +40,8 @@ class NVIDIAConfig(BaseModel):
default_factory=lambda: os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com"),
description="A base url for accessing the NVIDIA NIM",
)
api_key: SecretStr = Field(
default_factory=lambda: SecretStr(os.getenv("NVIDIA_API_KEY", "")),
api_key: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("NVIDIA_API_KEY", "")),
description="The NVIDIA API key, only needed of using the hosted service",
)
timeout: int = Field(

View file

@ -6,22 +6,21 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class OpenAIProviderDataValidator(BaseModel):
openai_api_key: SecretStr = Field(
default=SecretStr(""),
openai_api_key: MySecretStr = Field(
description="API key for OpenAI models",
)
@json_schema_type
class OpenAIConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="API key for OpenAI models",
)
base_url: str = Field(

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -18,8 +19,7 @@ class PassthroughImplConfig(BaseModel):
description="The URL for the passthrough endpoint",
)
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="API Key for the passthrouth endpoint",
)

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -17,8 +18,7 @@ class RunpodImplConfig(BaseModel):
default=None,
description="The URL for the Runpod model serving endpoint",
)
api_token: SecretStr = Field(
default=SecretStr(""),
api_token: MySecretStr = Field(
description="The API token",
)

View file

@ -6,14 +6,14 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class SambaNovaProviderDataValidator(BaseModel):
sambanova_api_key: SecretStr = Field(
default=SecretStr(""),
sambanova_api_key: MySecretStr = Field(
description="Sambanova Cloud API key",
)
@ -24,8 +24,7 @@ class SambaNovaImplConfig(BaseModel):
default="https://api.sambanova.ai/v1",
description="The URL for the SambaNova AI server",
)
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The SambaNova cloud API Key",
)

View file

@ -5,8 +5,9 @@
# the root directory of this source tree.
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -32,8 +33,7 @@ class InferenceEndpointImplConfig(BaseModel):
endpoint_name: str = Field(
description="The name of the Hugging Face Inference Endpoint in the format of '{namespace}/{endpoint_name}' (e.g. 'my-cool-org/meta-llama-3-1-8b-instruct-rce'). Namespace is optional and will default to the user account if not provided.",
)
api_token: SecretStr | None = Field(
default=None,
api_token: MySecretStr = Field(
description="Your Hugging Face user access token (will default to locally saved token if not provided)",
)
@ -55,8 +55,7 @@ class InferenceAPIImplConfig(BaseModel):
huggingface_repo: str = Field(
description="The model ID of the model on the Hugging Face Hub (e.g. 'meta-llama/Meta-Llama-3.1-70B-Instruct')",
)
api_token: SecretStr | None = Field(
default=None,
api_token: MySecretStr = Field(
description="Your Hugging Face user access token (will default to locally saved token if not provided)",
)

View file

@ -8,7 +8,6 @@
from collections.abc import AsyncGenerator
from huggingface_hub import AsyncInferenceClient, HfApi
from pydantic import SecretStr
from llama_stack.apis.common.content_types import (
InterleavedContent,
@ -35,6 +34,7 @@ from llama_stack.apis.inference import (
)
from llama_stack.apis.models import Model
from llama_stack.apis.models.models import ModelType
from llama_stack.core.secret_types import MySecretStr
from llama_stack.log import get_logger
from llama_stack.models.llama.sku_list import all_registered_models
from llama_stack.providers.datatypes import ModelsProtocolPrivate
@ -79,7 +79,7 @@ class _HfAdapter(
ModelsProtocolPrivate,
):
url: str
api_key: SecretStr
api_key: MySecretStr
hf_client: AsyncInferenceClient
max_tokens: int
@ -337,7 +337,7 @@ class TGIAdapter(_HfAdapter):
self.max_tokens = endpoint_info["max_total_tokens"]
self.model_id = endpoint_info["model_id"]
self.url = f"{config.url.rstrip('/')}/v1"
self.api_key = SecretStr("NO_KEY")
self.api_key = MySecretStr("NO_KEY")
class InferenceAPIAdapter(_HfAdapter):

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import Field, SecretStr
from pydantic import Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.inference.model_registry import RemoteInferenceProviderConfig
from llama_stack.schema_utils import json_schema_type
@ -18,8 +19,7 @@ class TogetherImplConfig(RemoteInferenceProviderConfig):
default="https://api.together.xyz/v1",
description="The URL for the Together AI server",
)
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Together AI API Key",
)

View file

@ -8,9 +8,9 @@ from typing import Any
import google.auth.transport.requests
from google.auth import default
from pydantic import SecretStr
from llama_stack.apis.inference import ChatCompletionRequest
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.inference.litellm_openai_mixin import (
LiteLLMOpenAIMixin,
)
@ -24,12 +24,12 @@ class VertexAIInferenceAdapter(OpenAIMixin, LiteLLMOpenAIMixin):
LiteLLMOpenAIMixin.__init__(
self,
litellm_provider_name="vertex_ai",
api_key_from_config=SecretStr(""), # Vertex AI uses ADC, not API keys
api_key_from_config=MySecretStr(None), # Vertex AI uses ADC, not API keys
provider_data_api_key_field="vertex_project", # Use project for validation
)
self.config = config
def get_api_key(self) -> SecretStr:
def get_api_key(self) -> MySecretStr:
"""
Get an access token for Vertex AI using Application Default Credentials.
@ -40,11 +40,11 @@ class VertexAIInferenceAdapter(OpenAIMixin, LiteLLMOpenAIMixin):
# Get default credentials - will read from GOOGLE_APPLICATION_CREDENTIALS
credentials, _ = default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
credentials.refresh(google.auth.transport.requests.Request())
return SecretStr(credentials.token)
return MySecretStr(credentials.token)
except Exception:
# If we can't get credentials, return empty string to let LiteLLM handle it
# This allows the LiteLLM mixin to work with ADC directly
return SecretStr("")
return MySecretStr("")
def get_base_url(self) -> str:
"""

View file

@ -4,14 +4,15 @@
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from .config import VLLMInferenceAdapterConfig
class VLLMProviderDataValidator(BaseModel):
vllm_api_token: SecretStr = Field(
default=SecretStr(""),
vllm_api_token: MySecretStr = Field(
description="API token for vLLM models",
)

View file

@ -6,8 +6,9 @@
from pathlib import Path
from pydantic import BaseModel, Field, SecretStr, field_validator
from pydantic import BaseModel, Field, field_validator
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -21,8 +22,7 @@ class VLLMInferenceAdapterConfig(BaseModel):
default=4096,
description="Maximum number of tokens to generate.",
)
api_token: SecretStr = Field(
default=SecretStr(""),
api_token: MySecretStr = Field(
description="The API token",
)
tls_verify: bool | str = Field(

View file

@ -7,8 +7,9 @@
import os
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
@ -24,8 +25,8 @@ class WatsonXConfig(BaseModel):
default_factory=lambda: os.getenv("WATSONX_BASE_URL", "https://us-south.ml.cloud.ibm.com"),
description="A base url for accessing the watsonx.ai",
)
api_key: SecretStr = Field(
default_factory=lambda: SecretStr(os.getenv("WATSONX_API_KEY", "")),
api_key: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("WATSONX_API_KEY", "")),
description="The watsonx API key",
)
project_id: str | None = Field(

View file

@ -9,14 +9,16 @@ from typing import Any
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
# TODO: add default values for all fields
class NvidiaPostTrainingConfig(BaseModel):
"""Configuration for NVIDIA Post Training implementation."""
api_key: str | None = Field(
default_factory=lambda: os.getenv("NVIDIA_API_KEY"),
api_key: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("NVIDIA_API_KEY", "")),
description="The NVIDIA API key.",
)

View file

@ -6,14 +6,14 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.schema_utils import json_schema_type
class SambaNovaProviderDataValidator(BaseModel):
sambanova_api_key: SecretStr = Field(
default=SecretStr(""),
sambanova_api_key: MySecretStr = Field(
description="Sambanova Cloud API key",
)
@ -24,8 +24,7 @@ class SambaNovaSafetyConfig(BaseModel):
default="https://api.sambanova.ai/v1",
description="The URL for the SambaNova AI server",
)
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The SambaNova cloud API Key",
)

View file

@ -6,14 +6,15 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class BingSearchToolConfig(BaseModel):
"""Configuration for Bing Search Tool Runtime"""
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Bing API key",
)
top_k: int = 3

View file

@ -6,12 +6,13 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class BraveSearchToolConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Brave Search API Key",
)
max_results: int = Field(

View file

@ -6,12 +6,13 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class TavilySearchToolConfig(BaseModel):
api_key: SecretStr = Field(
default=SecretStr(""),
api_key: MySecretStr = Field(
description="The Tavily Search API Key",
)
max_results: int = Field(

View file

@ -6,13 +6,17 @@
from typing import Any
from pydantic import BaseModel
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class WolframAlphaToolConfig(BaseModel):
"""Configuration for WolframAlpha Tool Runtime"""
api_key: str | None = None
api_key: MySecretStr = Field(
description="The WolframAlpha API Key",
)
@classmethod
def sample_run_config(cls, __distro_dir__: str, **kwargs: Any) -> dict[str, Any]:

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.kvstore.config import (
KVStoreConfig,
SqliteKVStoreConfig,
@ -21,7 +22,7 @@ class PGVectorVectorIOConfig(BaseModel):
port: int | None = Field(default=5432)
db: str | None = Field(default="postgres")
user: str | None = Field(default="postgres")
password: SecretStr = Field(default=SecretStr("mysecretpassword"))
password: MySecretStr = Field(default=MySecretStr("mysecretpassword"))
kvstore: KVStoreConfig | None = Field(description="Config for KV store backend (SQLite only for now)", default=None)
@classmethod

View file

@ -6,8 +6,9 @@
from typing import Any
from pydantic import BaseModel
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.kvstore.config import (
KVStoreConfig,
SqliteKVStoreConfig,
@ -23,7 +24,9 @@ class QdrantVectorIOConfig(BaseModel):
grpc_port: int = 6334
prefer_grpc: bool = False
https: bool | None = None
api_key: str | None = None
api_key: MySecretStr = Field(
description="The API key for the Qdrant instance",
)
prefix: str | None = None
timeout: int | None = None
host: str | None = None

View file

@ -6,7 +6,9 @@
import os
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
class BedrockBaseConfig(BaseModel):
@ -14,12 +16,12 @@ class BedrockBaseConfig(BaseModel):
default_factory=lambda: os.getenv("AWS_ACCESS_KEY_ID"),
description="The AWS access key to use. Default use environment variable: AWS_ACCESS_KEY_ID",
)
aws_secret_access_key: SecretStr = Field(
default_factory=lambda: SecretStr(os.getenv("AWS_SECRET_ACCESS_KEY", "")),
aws_secret_access_key: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("AWS_SECRET_ACCESS_KEY", "")),
description="The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY",
)
aws_session_token: SecretStr = Field(
default_factory=lambda: SecretStr(os.getenv("AWS_SESSION_TOKEN", "")),
aws_session_token: MySecretStr = Field(
default_factory=lambda: MySecretStr(os.getenv("AWS_SESSION_TOKEN", "")),
description="The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN",
)
region_name: str | None = Field(

View file

@ -8,7 +8,6 @@ from collections.abc import AsyncGenerator, AsyncIterator
from typing import Any
import litellm
from pydantic import SecretStr
from llama_stack.apis.common.content_types import (
InterleavedContent,
@ -40,6 +39,7 @@ from llama_stack.apis.inference import (
ToolPromptFormat,
)
from llama_stack.core.request_headers import NeedsRequestProviderData
from llama_stack.core.secret_types import MySecretStr
from llama_stack.log import get_logger
from llama_stack.providers.utils.inference.model_registry import ModelRegistryHelper, ProviderModelEntry
from llama_stack.providers.utils.inference.openai_compat import (
@ -69,7 +69,7 @@ class LiteLLMOpenAIMixin(
def __init__(
self,
litellm_provider_name: str,
api_key_from_config: SecretStr,
api_key_from_config: MySecretStr,
provider_data_api_key_field: str,
model_entries: list[ProviderModelEntry] | None = None,
openai_compat_api_base: str | None = None,
@ -255,7 +255,7 @@ class LiteLLMOpenAIMixin(
**get_sampling_options(request.sampling_params),
}
def get_api_key(self) -> SecretStr:
def get_api_key(self) -> MySecretStr:
provider_data = self.get_request_provider_data()
key_field = self.provider_data_api_key_field
if provider_data and getattr(provider_data, key_field, None):

View file

@ -11,7 +11,6 @@ from collections.abc import AsyncIterator
from typing import Any
from openai import NOT_GIVEN, AsyncOpenAI
from pydantic import SecretStr
from llama_stack.apis.inference import (
Model,
@ -25,6 +24,7 @@ from llama_stack.apis.inference import (
OpenAIResponseFormatParam,
)
from llama_stack.apis.models import ModelType
from llama_stack.core.secret_types import MySecretStr
from llama_stack.log import get_logger
from llama_stack.providers.utils.inference.model_registry import ModelRegistryHelper
from llama_stack.providers.utils.inference.openai_compat import prepare_openai_completion_params
@ -71,14 +71,14 @@ class OpenAIMixin(ModelRegistryHelper, ABC):
allowed_models: list[str] = []
@abstractmethod
def get_api_key(self) -> SecretStr:
def get_api_key(self) -> MySecretStr:
"""
Get the API key.
This method must be implemented by child classes to provide the API key
for authenticating with the OpenAI API or compatible endpoints.
:return: The API key as a SecretStr
:return: The API key as a MySecretStr
"""
pass
@ -114,7 +114,7 @@ class OpenAIMixin(ModelRegistryHelper, ABC):
implemented by child classes.
"""
return AsyncOpenAI(
api_key=self.get_api_key(),
api_key=self.get_api_key().get_secret_value(),
base_url=self.get_base_url(),
**self.get_extra_client_params(),
)

View file

@ -8,8 +8,9 @@ import re
from enum import Enum
from typing import Annotated, Literal
from pydantic import BaseModel, Field, SecretStr, field_validator
from pydantic import BaseModel, Field, field_validator
from llama_stack.core.secret_types import MySecretStr
from llama_stack.core.utils.config_dirs import RUNTIME_BASE_DIR
@ -74,7 +75,7 @@ class PostgresKVStoreConfig(CommonConfig):
port: int = 5432
db: str = "llamastack"
user: str
password: SecretStr = SecretStr("")
password: MySecretStr = MySecretStr("")
ssl_mode: str | None = None
ca_cert_path: str | None = None
table_name: str = "llamastack_kvstore"
@ -118,7 +119,7 @@ class MongoDBKVStoreConfig(CommonConfig):
port: int = 27017
db: str = "llamastack"
user: str | None = None
password: SecretStr = SecretStr("")
password: MySecretStr = MySecretStr("")
collection_name: str = "llamastack_kvstore"
@classmethod

View file

@ -9,8 +9,9 @@ from enum import StrEnum
from pathlib import Path
from typing import Annotated, Literal
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.secret_types import MySecretStr
from llama_stack.core.utils.config_dirs import RUNTIME_BASE_DIR
from .api import SqlStore
@ -63,7 +64,7 @@ class PostgresSqlStoreConfig(SqlAlchemySqlStoreConfig):
port: int = 5432
db: str = "llamastack"
user: str
password: SecretStr = SecretStr("")
password: MySecretStr = MySecretStr("")
@property
def engine_str(self) -> str:

View file

@ -8,19 +8,20 @@ import json
from unittest.mock import MagicMock
import pytest
from pydantic import BaseModel, Field, SecretStr
from pydantic import BaseModel, Field
from llama_stack.core.request_headers import request_provider_data_context
from llama_stack.core.secret_types import MySecretStr
from llama_stack.providers.utils.inference.litellm_openai_mixin import LiteLLMOpenAIMixin
# Test fixtures and helper classes
class TestConfig(BaseModel):
api_key: SecretStr | None = Field(default=None)
api_key: MySecretStr | None = Field(default=None)
class TestProviderDataValidator(BaseModel):
test_api_key: SecretStr | None = Field(default=None)
test_api_key: MySecretStr | None = Field(default=None)
class TestLiteLLMAdapter(LiteLLMOpenAIMixin):
@ -36,7 +37,7 @@ class TestLiteLLMAdapter(LiteLLMOpenAIMixin):
@pytest.fixture
def adapter_with_config_key():
"""Fixture to create adapter with API key in config"""
config = TestConfig(api_key=SecretStr("config-api-key"))
config = TestConfig(api_key=MySecretStr("config-api-key"))
adapter = TestLiteLLMAdapter(config)
adapter.__provider_spec__ = MagicMock()
adapter.__provider_spec__.provider_data_validator = (

View file

@ -7,9 +7,14 @@
import os
from unittest.mock import MagicMock, patch
from pydantic import SecretStr
from llama_stack.core.secret_types import MySecretStr
# Wrapper for backward compatibility in tests
def replace_env_vars_compat(config, path=""):
return replace_env_vars_compat(config, path, None, None)
from llama_stack.core.stack import replace_env_vars
from llama_stack.providers.remote.inference.openai.config import OpenAIConfig
from llama_stack.providers.remote.inference.openai.openai import OpenAIInferenceAdapter
@ -37,7 +42,7 @@ class TestOpenAIBaseURLConfig:
"""Test that the adapter uses base URL from OPENAI_BASE_URL environment variable."""
# Use sample_run_config which has proper environment variable syntax
config_data = OpenAIConfig.sample_run_config(api_key="test-key")
processed_config = replace_env_vars(config_data)
processed_config = replace_env_vars_compat(config_data)
config = OpenAIConfig.model_validate(processed_config)
adapter = OpenAIInferenceAdapter(config)
@ -61,14 +66,14 @@ class TestOpenAIBaseURLConfig:
adapter = OpenAIInferenceAdapter(config)
# Mock the get_api_key method since it's delegated to LiteLLMOpenAIMixin
adapter.get_api_key = MagicMock(return_value=SecretStr("test-key"))
adapter.get_api_key = MagicMock(return_value=MySecretStr("test-key"))
# Access the client property to trigger AsyncOpenAI initialization
_ = adapter.client
# Verify AsyncOpenAI was called with the correct base_url
mock_openai_class.assert_called_once_with(
api_key=SecretStr("test-key"),
api_key=MySecretStr("test-key"),
base_url=custom_url,
)
@ -80,7 +85,7 @@ class TestOpenAIBaseURLConfig:
adapter = OpenAIInferenceAdapter(config)
# Mock the get_api_key method
adapter.get_api_key = MagicMock(return_value=SecretStr("test-key"))
adapter.get_api_key = MagicMock(return_value=MySecretStr("test-key"))
# Mock a model object that will be returned by models.list()
mock_model = MagicMock()
@ -103,7 +108,7 @@ class TestOpenAIBaseURLConfig:
# Verify the client was created with the custom URL
mock_openai_class.assert_called_with(
api_key=SecretStr("test-key"),
api_key=MySecretStr("test-key"),
base_url=custom_url,
)
@ -116,12 +121,12 @@ class TestOpenAIBaseURLConfig:
"""Test that setting OPENAI_BASE_URL environment variable affects where model availability is checked."""
# Use sample_run_config which has proper environment variable syntax
config_data = OpenAIConfig.sample_run_config(api_key="test-key")
processed_config = replace_env_vars(config_data)
processed_config = replace_env_vars_compat(config_data)
config = OpenAIConfig.model_validate(processed_config)
adapter = OpenAIInferenceAdapter(config)
# Mock the get_api_key method
adapter.get_api_key = MagicMock(return_value=SecretStr("test-key"))
adapter.get_api_key = MagicMock(return_value=MySecretStr("test-key"))
# Mock a model object that will be returned by models.list()
mock_model = MagicMock()
@ -144,6 +149,6 @@ class TestOpenAIBaseURLConfig:
# Verify the client was created with the environment variable URL
mock_openai_class.assert_called_with(
api_key=SecretStr("test-key"),
api_key=MySecretStr("test-key"),
base_url="https://proxy.openai.com/v1",
)

View file

@ -8,7 +8,10 @@ import os
import pytest
from llama_stack.core.stack import replace_env_vars
# Wrapper for backward compatibility in tests
def replace_env_vars_compat(config, path=""):
return replace_env_vars_compat(config, path, None, None)
@pytest.fixture
@ -32,52 +35,54 @@ def setup_env_vars():
def test_simple_replacement(setup_env_vars):
assert replace_env_vars("${env.TEST_VAR}") == "test_value"
assert replace_env_vars_compat("${env.TEST_VAR}") == "test_value"
def test_default_value_when_not_set(setup_env_vars):
assert replace_env_vars("${env.NOT_SET:=default}") == "default"
assert replace_env_vars_compat("${env.NOT_SET:=default}") == "default"
def test_default_value_when_set(setup_env_vars):
assert replace_env_vars("${env.TEST_VAR:=default}") == "test_value"
assert replace_env_vars_compat("${env.TEST_VAR:=default}") == "test_value"
def test_default_value_when_empty(setup_env_vars):
assert replace_env_vars("${env.EMPTY_VAR:=default}") == "default"
assert replace_env_vars_compat("${env.EMPTY_VAR:=default}") == "default"
def test_none_value_when_empty(setup_env_vars):
assert replace_env_vars("${env.EMPTY_VAR:=}") is None
assert replace_env_vars_compat("${env.EMPTY_VAR:=}") is None
def test_value_when_set(setup_env_vars):
assert replace_env_vars("${env.TEST_VAR:=}") == "test_value"
assert replace_env_vars_compat("${env.TEST_VAR:=}") == "test_value"
def test_empty_var_no_default(setup_env_vars):
assert replace_env_vars("${env.EMPTY_VAR_NO_DEFAULT:+}") is None
assert replace_env_vars_compat("${env.EMPTY_VAR_NO_DEFAULT:+}") is None
def test_conditional_value_when_set(setup_env_vars):
assert replace_env_vars("${env.TEST_VAR:+conditional}") == "conditional"
assert replace_env_vars_compat("${env.TEST_VAR:+conditional}") == "conditional"
def test_conditional_value_when_not_set(setup_env_vars):
assert replace_env_vars("${env.NOT_SET:+conditional}") is None
assert replace_env_vars_compat("${env.NOT_SET:+conditional}") is None
def test_conditional_value_when_empty(setup_env_vars):
assert replace_env_vars("${env.EMPTY_VAR:+conditional}") is None
assert replace_env_vars_compat("${env.EMPTY_VAR:+conditional}") is None
def test_conditional_value_with_zero(setup_env_vars):
assert replace_env_vars("${env.ZERO_VAR:+conditional}") == "conditional"
assert replace_env_vars_compat("${env.ZERO_VAR:+conditional}") == "conditional"
def test_mixed_syntax(setup_env_vars):
assert replace_env_vars("${env.TEST_VAR:=default} and ${env.NOT_SET:+conditional}") == "test_value and "
assert replace_env_vars("${env.NOT_SET:=default} and ${env.TEST_VAR:+conditional}") == "default and conditional"
assert replace_env_vars_compat("${env.TEST_VAR:=default} and ${env.NOT_SET:+conditional}") == "test_value and "
assert (
replace_env_vars_compat("${env.NOT_SET:=default} and ${env.TEST_VAR:+conditional}") == "default and conditional"
)
def test_nested_structures(setup_env_vars):
@ -87,11 +92,11 @@ def test_nested_structures(setup_env_vars):
"key3": {"nested": "${env.NOT_SET:+conditional}"},
}
expected = {"key1": "test_value", "key2": ["default", "conditional"], "key3": {"nested": None}}
assert replace_env_vars(data) == expected
assert replace_env_vars_compat(data) == expected
def test_explicit_strings_preserved(setup_env_vars):
# Explicit strings that look like numbers/booleans should remain strings
data = {"port": "8080", "enabled": "true", "count": "123", "ratio": "3.14"}
expected = {"port": "8080", "enabled": "true", "count": "123", "ratio": "3.14"}
assert replace_env_vars(data) == expected
assert replace_env_vars_compat(data) == expected