From bc64635835ab047cc3bf968d69329de9d83f2f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Han?= Date: Thu, 25 Sep 2025 10:27:41 +0200 Subject: [PATCH] feat: load config class when doing variable substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../providers/datasetio/remote_nvidia.mdx | 2 +- docs/docs/providers/files/remote_s3.mdx | 2 +- .../providers/inference/remote_anthropic.mdx | 2 +- .../docs/providers/inference/remote_azure.mdx | 2 +- .../providers/inference/remote_bedrock.mdx | 4 +- .../providers/inference/remote_cerebras.mdx | 2 +- .../providers/inference/remote_databricks.mdx | 2 +- .../providers/inference/remote_fireworks.mdx | 2 +- .../providers/inference/remote_gemini.mdx | 2 +- docs/docs/providers/inference/remote_groq.mdx | 2 +- .../inference/remote_hf_endpoint.mdx | 2 +- .../inference/remote_hf_serverless.mdx | 2 +- .../inference/remote_llama-openai-compat.mdx | 2 +- .../providers/inference/remote_nvidia.mdx | 2 +- .../providers/inference/remote_openai.mdx | 2 +- .../inference/remote_passthrough.mdx | 2 +- .../providers/inference/remote_runpod.mdx | 2 +- .../providers/inference/remote_sambanova.mdx | 2 +- .../providers/inference/remote_together.mdx | 2 +- docs/docs/providers/inference/remote_vllm.mdx | 2 +- .../providers/inference/remote_watsonx.mdx | 2 +- .../providers/post_training/remote_nvidia.mdx | 2 +- docs/docs/providers/safety/remote_bedrock.mdx | 4 +- .../providers/safety/remote_sambanova.mdx | 2 +- .../providers/scoring/inline_braintrust.mdx | 2 +- .../tool_runtime/remote_bing-search.mdx | 2 +- .../tool_runtime/remote_brave-search.mdx | 2 +- .../tool_runtime/remote_tavily-search.mdx | 2 +- .../tool_runtime/remote_wolfram-alpha.mdx | 2 +- .../providers/vector_io/remote_pgvector.mdx | 2 +- .../providers/vector_io/remote_qdrant.mdx | 2 +- llama_stack/cli/stack/_build.py | 2 +- llama_stack/core/configure.py | 4 +- llama_stack/core/library_client.py | 5 +- llama_stack/core/secret_types.py | 21 +++ llama_stack/core/server/server.py | 11 +- llama_stack/core/stack.py | 136 ++++++++++++++++-- .../inline/scoring/braintrust/__init__.py | 5 +- .../inline/scoring/braintrust/braintrust.py | 5 +- .../inline/scoring/braintrust/config.py | 7 +- .../meta_reference/console_span_processor.py | 3 +- .../remote/datasetio/nvidia/config.py | 6 +- .../providers/remote/files/s3/config.py | 7 +- .../remote/inference/anthropic/config.py | 9 +- .../remote/inference/azure/config.py | 7 +- .../remote/inference/cerebras/config.py | 11 +- .../remote/inference/databricks/config.py | 6 +- .../remote/inference/fireworks/config.py | 6 +- .../remote/inference/gemini/config.py | 9 +- .../providers/remote/inference/groq/config.py | 9 +- .../inference/llama_openai_compat/config.py | 9 +- .../remote/inference/nvidia/config.py | 7 +- .../remote/inference/openai/config.py | 9 +- .../remote/inference/passthrough/config.py | 6 +- .../remote/inference/runpod/config.py | 6 +- .../remote/inference/sambanova/config.py | 9 +- .../providers/remote/inference/tgi/config.py | 9 +- .../providers/remote/inference/tgi/tgi.py | 6 +- .../remote/inference/together/config.py | 6 +- .../remote/inference/vertexai/vertexai.py | 10 +- .../remote/inference/vllm/__init__.py | 7 +- .../providers/remote/inference/vllm/config.py | 6 +- .../remote/inference/watsonx/config.py | 7 +- .../remote/post_training/nvidia/config.py | 6 +- .../remote/safety/sambanova/config.py | 9 +- .../remote/tool_runtime/bing_search/config.py | 7 +- .../tool_runtime/brave_search/config.py | 7 +- .../tool_runtime/tavily_search/config.py | 7 +- .../tool_runtime/wolfram_alpha/config.py | 8 +- .../remote/vector_io/pgvector/config.py | 5 +- .../remote/vector_io/qdrant/config.py | 7 +- llama_stack/providers/utils/bedrock/config.py | 12 +- .../utils/inference/litellm_openai_mixin.py | 6 +- .../providers/utils/inference/openai_mixin.py | 8 +- llama_stack/providers/utils/kvstore/config.py | 7 +- .../providers/utils/sqlstore/sqlstore.py | 5 +- .../inference/test_litellm_openai_mixin.py | 9 +- .../inference/test_openai_base_url_config.py | 25 ++-- tests/unit/server/test_replace_env_vars.py | 37 ++--- 79 files changed, 381 insertions(+), 216 deletions(-) create mode 100644 llama_stack/core/secret_types.py diff --git a/docs/docs/providers/datasetio/remote_nvidia.mdx b/docs/docs/providers/datasetio/remote_nvidia.mdx index 35a7dacee..ba5522fce 100644 --- a/docs/docs/providers/datasetio/remote_nvidia.mdx +++ b/docs/docs/providers/datasetio/remote_nvidia.mdx @@ -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` | `` | 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` | `` | No | http://nemo.test | Base URL for the NeMo Dataset API | diff --git a/docs/docs/providers/files/remote_s3.mdx b/docs/docs/providers/files/remote_s3.mdx index adf4bced0..9de31df44 100644 --- a/docs/docs/providers/files/remote_s3.mdx +++ b/docs/docs/providers/files/remote_s3.mdx @@ -17,7 +17,7 @@ AWS S3-based file storage provider for scalable cloud file management with metad | `bucket_name` | `` | No | | S3 bucket name to store files | | `region` | `` | 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` | `` | No | | AWS secret access key (optional if using IAM roles) | +| `aws_secret_access_key` | `` | 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` | `` | 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 | diff --git a/docs/docs/providers/inference/remote_anthropic.mdx b/docs/docs/providers/inference/remote_anthropic.mdx index f795ad3f1..72deb298c 100644 --- a/docs/docs/providers/inference/remote_anthropic.mdx +++ b/docs/docs/providers/inference/remote_anthropic.mdx @@ -14,7 +14,7 @@ Anthropic inference provider for accessing Claude models and Anthropic's AI serv | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | API key for Anthropic models | +| `api_key` | `` | No | | API key for Anthropic models | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_azure.mdx b/docs/docs/providers/inference/remote_azure.mdx index 0eb0ea755..1085e4ad4 100644 --- a/docs/docs/providers/inference/remote_azure.mdx +++ b/docs/docs/providers/inference/remote_azure.mdx @@ -21,7 +21,7 @@ https://learn.microsoft.com/en-us/azure/ai-foundry/openai/overview | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | Azure API key for Azure | +| `api_key` | `` | No | | Azure API key for Azure | | `api_base` | `` | 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) | diff --git a/docs/docs/providers/inference/remote_bedrock.mdx b/docs/docs/providers/inference/remote_bedrock.mdx index ff1ed5ad8..627be48e5 100644 --- a/docs/docs/providers/inference/remote_bedrock.mdx +++ b/docs/docs/providers/inference/remote_bedrock.mdx @@ -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` | `` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY | -| `aws_session_token` | `` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN | +| `aws_secret_access_key` | `` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY | +| `aws_session_token` | `` | 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 | diff --git a/docs/docs/providers/inference/remote_cerebras.mdx b/docs/docs/providers/inference/remote_cerebras.mdx index d9cc93aef..7c96e5115 100644 --- a/docs/docs/providers/inference/remote_cerebras.mdx +++ b/docs/docs/providers/inference/remote_cerebras.mdx @@ -15,7 +15,7 @@ Cerebras inference provider for running models on Cerebras Cloud platform. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `base_url` | `` | No | https://api.cerebras.ai | Base URL for the Cerebras API | -| `api_key` | `` | No | | Cerebras API Key | +| `api_key` | `` | No | | Cerebras API Key | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_databricks.mdx b/docs/docs/providers/inference/remote_databricks.mdx index 7f736db9d..fb3783720 100644 --- a/docs/docs/providers/inference/remote_databricks.mdx +++ b/docs/docs/providers/inference/remote_databricks.mdx @@ -15,7 +15,7 @@ Databricks inference provider for running models on Databricks' unified analytic | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | | The URL for the Databricks model serving endpoint | -| `api_token` | `` | No | | The Databricks API token | +| `api_token` | `` | No | | The Databricks API token | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_fireworks.mdx b/docs/docs/providers/inference/remote_fireworks.mdx index 0f37ccbc2..73698f28c 100644 --- a/docs/docs/providers/inference/remote_fireworks.mdx +++ b/docs/docs/providers/inference/remote_fireworks.mdx @@ -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` | `` | No | https://api.fireworks.ai/inference/v1 | The URL for the Fireworks server | -| `api_key` | `` | No | | The Fireworks.ai API Key | +| `api_key` | `` | No | | The Fireworks.ai API Key | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_gemini.mdx b/docs/docs/providers/inference/remote_gemini.mdx index d9a2f3e8d..538a9f29b 100644 --- a/docs/docs/providers/inference/remote_gemini.mdx +++ b/docs/docs/providers/inference/remote_gemini.mdx @@ -14,7 +14,7 @@ Google Gemini inference provider for accessing Gemini models and Google's AI ser | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | API key for Gemini models | +| `api_key` | `` | No | | API key for Gemini models | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_groq.mdx b/docs/docs/providers/inference/remote_groq.mdx index b6d29496e..2f80ab7ca 100644 --- a/docs/docs/providers/inference/remote_groq.mdx +++ b/docs/docs/providers/inference/remote_groq.mdx @@ -14,7 +14,7 @@ Groq inference provider for ultra-fast inference using Groq's LPU technology. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | The Groq API key | +| `api_key` | `` | No | | The Groq API key | | `url` | `` | No | https://api.groq.com | The URL for the Groq AI server | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_hf_endpoint.mdx b/docs/docs/providers/inference/remote_hf_endpoint.mdx index 771b24f8d..93468fdbc 100644 --- a/docs/docs/providers/inference/remote_hf_endpoint.mdx +++ b/docs/docs/providers/inference/remote_hf_endpoint.mdx @@ -15,7 +15,7 @@ HuggingFace Inference Endpoints provider for dedicated model serving. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `endpoint_name` | `` | No | | 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` | `pydantic.types.SecretStr \| None` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) | +| `api_token` | `` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_hf_serverless.mdx b/docs/docs/providers/inference/remote_hf_serverless.mdx index 1a89b8e3e..80753ed05 100644 --- a/docs/docs/providers/inference/remote_hf_serverless.mdx +++ b/docs/docs/providers/inference/remote_hf_serverless.mdx @@ -15,7 +15,7 @@ HuggingFace Inference API serverless provider for on-demand model inference. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `huggingface_repo` | `` | 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` | `` | No | | Your Hugging Face user access token (will default to locally saved token if not provided) | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_llama-openai-compat.mdx b/docs/docs/providers/inference/remote_llama-openai-compat.mdx index 491774844..478b7dadb 100644 --- a/docs/docs/providers/inference/remote_llama-openai-compat.mdx +++ b/docs/docs/providers/inference/remote_llama-openai-compat.mdx @@ -14,7 +14,7 @@ Llama OpenAI-compatible provider for using Llama models with OpenAI API format. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | The Llama API key | +| `api_key` | `` | No | | The Llama API key | | `openai_compat_api_base` | `` | No | https://api.llama.com/compat/v1/ | The URL for the Llama API server | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_nvidia.mdx b/docs/docs/providers/inference/remote_nvidia.mdx index 9aee22e9a..c51da9547 100644 --- a/docs/docs/providers/inference/remote_nvidia.mdx +++ b/docs/docs/providers/inference/remote_nvidia.mdx @@ -15,7 +15,7 @@ NVIDIA inference provider for accessing NVIDIA NIM models and AI services. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | https://integrate.api.nvidia.com | A base url for accessing the NVIDIA NIM | -| `api_key` | `` | No | | The NVIDIA API key, only needed of using the hosted service | +| `api_key` | `` | No | | The NVIDIA API key, only needed of using the hosted service | | `timeout` | `` | No | 60 | Timeout for the HTTP requests | | `append_api_version` | `` | No | True | When set to false, the API version will not be appended to the base_url. By default, it is true. | diff --git a/docs/docs/providers/inference/remote_openai.mdx b/docs/docs/providers/inference/remote_openai.mdx index f82bea154..79e9d9ccc 100644 --- a/docs/docs/providers/inference/remote_openai.mdx +++ b/docs/docs/providers/inference/remote_openai.mdx @@ -14,7 +14,7 @@ OpenAI inference provider for accessing GPT models and other OpenAI services. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | API key for OpenAI models | +| `api_key` | `` | No | | API key for OpenAI models | | `base_url` | `` | No | https://api.openai.com/v1 | Base URL for OpenAI API | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_passthrough.mdx b/docs/docs/providers/inference/remote_passthrough.mdx index efaa01d04..f35f746db 100644 --- a/docs/docs/providers/inference/remote_passthrough.mdx +++ b/docs/docs/providers/inference/remote_passthrough.mdx @@ -15,7 +15,7 @@ Passthrough inference provider for connecting to any external inference service | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | | The URL for the passthrough endpoint | -| `api_key` | `` | No | | API Key for the passthrouth endpoint | +| `api_key` | `` | No | | API Key for the passthrouth endpoint | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_runpod.mdx b/docs/docs/providers/inference/remote_runpod.mdx index 1def462f6..8028f0228 100644 --- a/docs/docs/providers/inference/remote_runpod.mdx +++ b/docs/docs/providers/inference/remote_runpod.mdx @@ -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` | `` | No | | The API token | +| `api_token` | `` | No | | The API token | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_sambanova.mdx b/docs/docs/providers/inference/remote_sambanova.mdx index b5d64e6d9..2a63cab16 100644 --- a/docs/docs/providers/inference/remote_sambanova.mdx +++ b/docs/docs/providers/inference/remote_sambanova.mdx @@ -15,7 +15,7 @@ SambaNova inference provider for running models on SambaNova's dataflow architec | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | https://api.sambanova.ai/v1 | The URL for the SambaNova AI server | -| `api_key` | `` | No | | The SambaNova cloud API Key | +| `api_key` | `` | No | | The SambaNova cloud API Key | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_together.mdx b/docs/docs/providers/inference/remote_together.mdx index 0669a1112..912cf0291 100644 --- a/docs/docs/providers/inference/remote_together.mdx +++ b/docs/docs/providers/inference/remote_together.mdx @@ -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` | `` | No | https://api.together.xyz/v1 | The URL for the Together AI server | -| `api_key` | `` | No | | The Together AI API Key | +| `api_key` | `` | No | | The Together AI API Key | ## Sample Configuration diff --git a/docs/docs/providers/inference/remote_vllm.mdx b/docs/docs/providers/inference/remote_vllm.mdx index 5f9af4db4..4db289376 100644 --- a/docs/docs/providers/inference/remote_vllm.mdx +++ b/docs/docs/providers/inference/remote_vllm.mdx @@ -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` | `` | No | 4096 | Maximum number of tokens to generate. | -| `api_token` | `` | No | | The API token | +| `api_token` | `` | 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` | `` | No | False | Whether to refresh models periodically | diff --git a/docs/docs/providers/inference/remote_watsonx.mdx b/docs/docs/providers/inference/remote_watsonx.mdx index c1ab57a7b..2584a78ac 100644 --- a/docs/docs/providers/inference/remote_watsonx.mdx +++ b/docs/docs/providers/inference/remote_watsonx.mdx @@ -15,7 +15,7 @@ IBM WatsonX inference provider for accessing AI models on IBM's WatsonX platform | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | https://us-south.ml.cloud.ibm.com | A base url for accessing the watsonx.ai | -| `api_key` | `` | No | | The watsonx API key | +| `api_key` | `` | No | | The watsonx API key | | `project_id` | `str \| None` | No | | The Project ID key | | `timeout` | `` | No | 60 | Timeout for the HTTP requests | diff --git a/docs/docs/providers/post_training/remote_nvidia.mdx b/docs/docs/providers/post_training/remote_nvidia.mdx index 448ac4c75..451b1b258 100644 --- a/docs/docs/providers/post_training/remote_nvidia.mdx +++ b/docs/docs/providers/post_training/remote_nvidia.mdx @@ -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` | `` | 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 | diff --git a/docs/docs/providers/safety/remote_bedrock.mdx b/docs/docs/providers/safety/remote_bedrock.mdx index e068f1fed..d5362ccd9 100644 --- a/docs/docs/providers/safety/remote_bedrock.mdx +++ b/docs/docs/providers/safety/remote_bedrock.mdx @@ -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` | `` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY | -| `aws_session_token` | `` | No | | The AWS session token to use. Default use environment variable: AWS_SESSION_TOKEN | +| `aws_secret_access_key` | `` | No | | The AWS secret access key to use. Default use environment variable: AWS_SECRET_ACCESS_KEY | +| `aws_session_token` | `` | 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 | diff --git a/docs/docs/providers/safety/remote_sambanova.mdx b/docs/docs/providers/safety/remote_sambanova.mdx index 3a5a0db7d..c63e2160c 100644 --- a/docs/docs/providers/safety/remote_sambanova.mdx +++ b/docs/docs/providers/safety/remote_sambanova.mdx @@ -15,7 +15,7 @@ SambaNova's safety provider for content moderation and safety filtering. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `url` | `` | No | https://api.sambanova.ai/v1 | The URL for the SambaNova AI server | -| `api_key` | `` | No | | The SambaNova cloud API Key | +| `api_key` | `` | No | | The SambaNova cloud API Key | ## Sample Configuration diff --git a/docs/docs/providers/scoring/inline_braintrust.mdx b/docs/docs/providers/scoring/inline_braintrust.mdx index 93779b7ac..b035c259a 100644 --- a/docs/docs/providers/scoring/inline_braintrust.mdx +++ b/docs/docs/providers/scoring/inline_braintrust.mdx @@ -14,7 +14,7 @@ Braintrust scoring provider for evaluation and scoring using the Braintrust plat | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `openai_api_key` | `` | No | | The OpenAI API Key | +| `openai_api_key` | `` | No | | The OpenAI API Key | ## Sample Configuration diff --git a/docs/docs/providers/tool_runtime/remote_bing-search.mdx b/docs/docs/providers/tool_runtime/remote_bing-search.mdx index b7dabdd47..02ed6af2c 100644 --- a/docs/docs/providers/tool_runtime/remote_bing-search.mdx +++ b/docs/docs/providers/tool_runtime/remote_bing-search.mdx @@ -14,7 +14,7 @@ Bing Search tool for web search capabilities using Microsoft's search engine. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | The Bing API key | +| `api_key` | `` | No | | The Bing API key | | `top_k` | `` | No | 3 | | ## Sample Configuration diff --git a/docs/docs/providers/tool_runtime/remote_brave-search.mdx b/docs/docs/providers/tool_runtime/remote_brave-search.mdx index 084ae20f5..ccb7f3e23 100644 --- a/docs/docs/providers/tool_runtime/remote_brave-search.mdx +++ b/docs/docs/providers/tool_runtime/remote_brave-search.mdx @@ -14,7 +14,7 @@ Brave Search tool for web search capabilities with privacy-focused results. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | The Brave Search API Key | +| `api_key` | `` | No | | The Brave Search API Key | | `max_results` | `` | No | 3 | The maximum number of results to return | ## Sample Configuration diff --git a/docs/docs/providers/tool_runtime/remote_tavily-search.mdx b/docs/docs/providers/tool_runtime/remote_tavily-search.mdx index 1c3429983..7947c42c8 100644 --- a/docs/docs/providers/tool_runtime/remote_tavily-search.mdx +++ b/docs/docs/providers/tool_runtime/remote_tavily-search.mdx @@ -14,7 +14,7 @@ Tavily Search tool for AI-optimized web search with structured results. | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| -| `api_key` | `` | No | | The Tavily Search API Key | +| `api_key` | `` | No | | The Tavily Search API Key | | `max_results` | `` | No | 3 | The maximum number of results to return | ## Sample Configuration diff --git a/docs/docs/providers/tool_runtime/remote_wolfram-alpha.mdx b/docs/docs/providers/tool_runtime/remote_wolfram-alpha.mdx index 96bc41789..2dd58f043 100644 --- a/docs/docs/providers/tool_runtime/remote_wolfram-alpha.mdx +++ b/docs/docs/providers/tool_runtime/remote_wolfram-alpha.mdx @@ -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` | `` | No | | The WolframAlpha API Key | ## Sample Configuration diff --git a/docs/docs/providers/vector_io/remote_pgvector.mdx b/docs/docs/providers/vector_io/remote_pgvector.mdx index 6d3157753..2853cddeb 100644 --- a/docs/docs/providers/vector_io/remote_pgvector.mdx +++ b/docs/docs/providers/vector_io/remote_pgvector.mdx @@ -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` | `` | No | ********** | | +| `password` | `` | 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 diff --git a/docs/docs/providers/vector_io/remote_qdrant.mdx b/docs/docs/providers/vector_io/remote_qdrant.mdx index c44a2b937..c344dfe2e 100644 --- a/docs/docs/providers/vector_io/remote_qdrant.mdx +++ b/docs/docs/providers/vector_io/remote_qdrant.mdx @@ -22,7 +22,7 @@ Please refer to the inline provider documentation. | `grpc_port` | `` | No | 6334 | | | `prefer_grpc` | `` | No | False | | | `https` | `bool \| None` | No | | | -| `api_key` | `str \| None` | No | | | +| `api_key` | `` | No | | The API key for the Qdrant instance | | `prefix` | `str \| None` | No | | | | `timeout` | `int \| None` | No | | | | `host` | `str \| None` | No | | | diff --git a/llama_stack/cli/stack/_build.py b/llama_stack/cli/stack/_build.py index b14e6fe55..1992ba292 100644 --- a/llama_stack/cli/stack/_build.py +++ b/llama_stack/cli/stack/_build.py @@ -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 diff --git a/llama_stack/core/configure.py b/llama_stack/core/configure.py index 64473c053..92ec66c49 100644 --- a/llama_stack/core/configure.py +++ b/llama_stack/core/configure.py @@ -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)) diff --git a/llama_stack/core/library_client.py b/llama_stack/core/library_client.py index e722e4de6..6f0f3fb2e 100644 --- a/llama_stack/core/library_client.py +++ b/llama_stack/core/library_client.py @@ -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 diff --git a/llama_stack/core/secret_types.py b/llama_stack/core/secret_types.py new file mode 100644 index 000000000..e1700d783 --- /dev/null +++ b/llama_stack/core/secret_types.py @@ -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] diff --git a/llama_stack/core/server/server.py b/llama_stack/core/server/server.py index 7d119c139..1c5a79f1c 100644 --- a/llama_stack/core/server/server.py +++ b/llama_stack/core/server/server.py @@ -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 diff --git a/llama_stack/core/stack.py b/llama_stack/core/stack.py index 3e14328a3..3b93f9913 100644 --- a/llama_stack/core/stack.py +++ b/llama_stack/core/stack.py @@ -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( diff --git a/llama_stack/providers/inline/scoring/braintrust/__init__.py b/llama_stack/providers/inline/scoring/braintrust/__init__.py index 2f3dce966..77c09c615 100644 --- a/llama_stack/providers/inline/scoring/braintrust/__init__.py +++ b/llama_stack/providers/inline/scoring/braintrust/__init__.py @@ -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( diff --git a/llama_stack/providers/inline/scoring/braintrust/braintrust.py b/llama_stack/providers/inline/scoring/braintrust/braintrust.py index 7f2b1e205..8d80b5920 100644 --- a/llama_stack/providers/inline/scoring/braintrust/braintrust.py +++ b/llama_stack/providers/inline/scoring/braintrust/braintrust.py @@ -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": }' ) - 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() diff --git a/llama_stack/providers/inline/scoring/braintrust/config.py b/llama_stack/providers/inline/scoring/braintrust/config.py index a2a52d610..3520ffb08 100644 --- a/llama_stack/providers/inline/scoring/braintrust/config.py +++ b/llama_stack/providers/inline/scoring/braintrust/config.py @@ -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", ) diff --git a/llama_stack/providers/inline/telemetry/meta_reference/console_span_processor.py b/llama_stack/providers/inline/telemetry/meta_reference/console_span_processor.py index 8beb3a841..a2eb896ea 100644 --- a/llama_stack/providers/inline/telemetry/meta_reference/console_span_processor.py +++ b/llama_stack/providers/inline/telemetry/meta_reference/console_span_processor.py @@ -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.""" diff --git a/llama_stack/providers/remote/datasetio/nvidia/config.py b/llama_stack/providers/remote/datasetio/nvidia/config.py index addce6c1f..aa1ac163d 100644 --- a/llama_stack/providers/remote/datasetio/nvidia/config.py +++ b/llama_stack/providers/remote/datasetio/nvidia/config.py @@ -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.", ) diff --git a/llama_stack/providers/remote/files/s3/config.py b/llama_stack/providers/remote/files/s3/config.py index b7935902d..e8fc452d8 100644 --- a/llama_stack/providers/remote/files/s3/config.py +++ b/llama_stack/providers/remote/files/s3/config.py @@ -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" diff --git a/llama_stack/providers/remote/inference/anthropic/config.py b/llama_stack/providers/remote/inference/anthropic/config.py index c28f05d24..eb77b328f 100644 --- a/llama_stack/providers/remote/inference/anthropic/config.py +++ b/llama_stack/providers/remote/inference/anthropic/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/azure/config.py b/llama_stack/providers/remote/inference/azure/config.py index fe9d61d53..ab53e85c2 100644 --- a/llama_stack/providers/remote/inference/azure/config.py +++ b/llama_stack/providers/remote/inference/azure/config.py @@ -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( diff --git a/llama_stack/providers/remote/inference/cerebras/config.py b/llama_stack/providers/remote/inference/cerebras/config.py index 0b737ea6c..5c5de27a5 100644 --- a/llama_stack/providers/remote/inference/cerebras/config.py +++ b/llama_stack/providers/remote/inference/cerebras/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/databricks/config.py b/llama_stack/providers/remote/inference/databricks/config.py index 67cd0480c..92b250034 100644 --- a/llama_stack/providers/remote/inference/databricks/config.py +++ b/llama_stack/providers/remote/inference/databricks/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/fireworks/config.py b/llama_stack/providers/remote/inference/fireworks/config.py index 5bf0fcd88..14f2dffc1 100644 --- a/llama_stack/providers/remote/inference/fireworks/config.py +++ b/llama_stack/providers/remote/inference/fireworks/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/gemini/config.py b/llama_stack/providers/remote/inference/gemini/config.py index 4a69bd064..a90fb93cb 100644 --- a/llama_stack/providers/remote/inference/gemini/config.py +++ b/llama_stack/providers/remote/inference/gemini/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/groq/config.py b/llama_stack/providers/remote/inference/groq/config.py index efc1f437b..c3c3fd580 100644 --- a/llama_stack/providers/remote/inference/groq/config.py +++ b/llama_stack/providers/remote/inference/groq/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/llama_openai_compat/config.py b/llama_stack/providers/remote/inference/llama_openai_compat/config.py index d20fed8e0..863988dd6 100644 --- a/llama_stack/providers/remote/inference/llama_openai_compat/config.py +++ b/llama_stack/providers/remote/inference/llama_openai_compat/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/nvidia/config.py b/llama_stack/providers/remote/inference/nvidia/config.py index 9ca2ffdd6..2f6dfe9f3 100644 --- a/llama_stack/providers/remote/inference/nvidia/config.py +++ b/llama_stack/providers/remote/inference/nvidia/config.py @@ -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( diff --git a/llama_stack/providers/remote/inference/openai/config.py b/llama_stack/providers/remote/inference/openai/config.py index b6f5f1a55..c8d6be9c2 100644 --- a/llama_stack/providers/remote/inference/openai/config.py +++ b/llama_stack/providers/remote/inference/openai/config.py @@ -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( diff --git a/llama_stack/providers/remote/inference/passthrough/config.py b/llama_stack/providers/remote/inference/passthrough/config.py index f57ce0e8a..dee3b9173 100644 --- a/llama_stack/providers/remote/inference/passthrough/config.py +++ b/llama_stack/providers/remote/inference/passthrough/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/runpod/config.py b/llama_stack/providers/remote/inference/runpod/config.py index 3c6c5afe9..eaabe4e33 100644 --- a/llama_stack/providers/remote/inference/runpod/config.py +++ b/llama_stack/providers/remote/inference/runpod/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/sambanova/config.py b/llama_stack/providers/remote/inference/sambanova/config.py index 4637cb49e..10e28c3ac 100644 --- a/llama_stack/providers/remote/inference/sambanova/config.py +++ b/llama_stack/providers/remote/inference/sambanova/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/tgi/config.py b/llama_stack/providers/remote/inference/tgi/config.py index 55136c8ba..a1f4c19f3 100644 --- a/llama_stack/providers/remote/inference/tgi/config.py +++ b/llama_stack/providers/remote/inference/tgi/config.py @@ -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)", ) diff --git a/llama_stack/providers/remote/inference/tgi/tgi.py b/llama_stack/providers/remote/inference/tgi/tgi.py index 27597900f..239fdce4e 100644 --- a/llama_stack/providers/remote/inference/tgi/tgi.py +++ b/llama_stack/providers/remote/inference/tgi/tgi.py @@ -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): diff --git a/llama_stack/providers/remote/inference/together/config.py b/llama_stack/providers/remote/inference/together/config.py index c15d42140..a2fa8f76e 100644 --- a/llama_stack/providers/remote/inference/together/config.py +++ b/llama_stack/providers/remote/inference/together/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/vertexai/vertexai.py b/llama_stack/providers/remote/inference/vertexai/vertexai.py index 581a00f29..dd1971df4 100644 --- a/llama_stack/providers/remote/inference/vertexai/vertexai.py +++ b/llama_stack/providers/remote/inference/vertexai/vertexai.py @@ -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: """ diff --git a/llama_stack/providers/remote/inference/vllm/__init__.py b/llama_stack/providers/remote/inference/vllm/__init__.py index 5b3ec6d5b..e47597c8f 100644 --- a/llama_stack/providers/remote/inference/vllm/__init__.py +++ b/llama_stack/providers/remote/inference/vllm/__init__.py @@ -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", ) diff --git a/llama_stack/providers/remote/inference/vllm/config.py b/llama_stack/providers/remote/inference/vllm/config.py index 708a39be1..7cc3ebebb 100644 --- a/llama_stack/providers/remote/inference/vllm/config.py +++ b/llama_stack/providers/remote/inference/vllm/config.py @@ -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( diff --git a/llama_stack/providers/remote/inference/watsonx/config.py b/llama_stack/providers/remote/inference/watsonx/config.py index a28de1226..64421c856 100644 --- a/llama_stack/providers/remote/inference/watsonx/config.py +++ b/llama_stack/providers/remote/inference/watsonx/config.py @@ -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( diff --git a/llama_stack/providers/remote/post_training/nvidia/config.py b/llama_stack/providers/remote/post_training/nvidia/config.py index 83d7b49e6..1730b779f 100644 --- a/llama_stack/providers/remote/post_training/nvidia/config.py +++ b/llama_stack/providers/remote/post_training/nvidia/config.py @@ -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.", ) diff --git a/llama_stack/providers/remote/safety/sambanova/config.py b/llama_stack/providers/remote/safety/sambanova/config.py index 814e5b1e5..ff58decfd 100644 --- a/llama_stack/providers/remote/safety/sambanova/config.py +++ b/llama_stack/providers/remote/safety/sambanova/config.py @@ -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", ) diff --git a/llama_stack/providers/remote/tool_runtime/bing_search/config.py b/llama_stack/providers/remote/tool_runtime/bing_search/config.py index 8ac4358ba..54b3a9d52 100644 --- a/llama_stack/providers/remote/tool_runtime/bing_search/config.py +++ b/llama_stack/providers/remote/tool_runtime/bing_search/config.py @@ -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 diff --git a/llama_stack/providers/remote/tool_runtime/brave_search/config.py b/llama_stack/providers/remote/tool_runtime/brave_search/config.py index ddc711c12..8fbc42154 100644 --- a/llama_stack/providers/remote/tool_runtime/brave_search/config.py +++ b/llama_stack/providers/remote/tool_runtime/brave_search/config.py @@ -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( diff --git a/llama_stack/providers/remote/tool_runtime/tavily_search/config.py b/llama_stack/providers/remote/tool_runtime/tavily_search/config.py index c0de93114..c2e31e5d3 100644 --- a/llama_stack/providers/remote/tool_runtime/tavily_search/config.py +++ b/llama_stack/providers/remote/tool_runtime/tavily_search/config.py @@ -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( diff --git a/llama_stack/providers/remote/tool_runtime/wolfram_alpha/config.py b/llama_stack/providers/remote/tool_runtime/wolfram_alpha/config.py index 457661c06..eea5ffdd2 100644 --- a/llama_stack/providers/remote/tool_runtime/wolfram_alpha/config.py +++ b/llama_stack/providers/remote/tool_runtime/wolfram_alpha/config.py @@ -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]: diff --git a/llama_stack/providers/remote/vector_io/pgvector/config.py b/llama_stack/providers/remote/vector_io/pgvector/config.py index 1c6d0ed52..ee4ef40d0 100644 --- a/llama_stack/providers/remote/vector_io/pgvector/config.py +++ b/llama_stack/providers/remote/vector_io/pgvector/config.py @@ -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 diff --git a/llama_stack/providers/remote/vector_io/qdrant/config.py b/llama_stack/providers/remote/vector_io/qdrant/config.py index ff5506236..58044e248 100644 --- a/llama_stack/providers/remote/vector_io/qdrant/config.py +++ b/llama_stack/providers/remote/vector_io/qdrant/config.py @@ -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 diff --git a/llama_stack/providers/utils/bedrock/config.py b/llama_stack/providers/utils/bedrock/config.py index 2a5c8e882..ae4018b80 100644 --- a/llama_stack/providers/utils/bedrock/config.py +++ b/llama_stack/providers/utils/bedrock/config.py @@ -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( diff --git a/llama_stack/providers/utils/inference/litellm_openai_mixin.py b/llama_stack/providers/utils/inference/litellm_openai_mixin.py index 8bdfbbbc9..a5d320e41 100644 --- a/llama_stack/providers/utils/inference/litellm_openai_mixin.py +++ b/llama_stack/providers/utils/inference/litellm_openai_mixin.py @@ -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): diff --git a/llama_stack/providers/utils/inference/openai_mixin.py b/llama_stack/providers/utils/inference/openai_mixin.py index 7fbd62ef6..4bb7a28c5 100644 --- a/llama_stack/providers/utils/inference/openai_mixin.py +++ b/llama_stack/providers/utils/inference/openai_mixin.py @@ -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(), ) diff --git a/llama_stack/providers/utils/kvstore/config.py b/llama_stack/providers/utils/kvstore/config.py index baab4e372..a8dd1a99a 100644 --- a/llama_stack/providers/utils/kvstore/config.py +++ b/llama_stack/providers/utils/kvstore/config.py @@ -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 diff --git a/llama_stack/providers/utils/sqlstore/sqlstore.py b/llama_stack/providers/utils/sqlstore/sqlstore.py index 6eaafccfe..3bcd8f40d 100644 --- a/llama_stack/providers/utils/sqlstore/sqlstore.py +++ b/llama_stack/providers/utils/sqlstore/sqlstore.py @@ -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: diff --git a/tests/unit/providers/inference/test_litellm_openai_mixin.py b/tests/unit/providers/inference/test_litellm_openai_mixin.py index cf7623dd1..48bf5ce38 100644 --- a/tests/unit/providers/inference/test_litellm_openai_mixin.py +++ b/tests/unit/providers/inference/test_litellm_openai_mixin.py @@ -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 = ( diff --git a/tests/unit/providers/inference/test_openai_base_url_config.py b/tests/unit/providers/inference/test_openai_base_url_config.py index 7bc908069..40c789073 100644 --- a/tests/unit/providers/inference/test_openai_base_url_config.py +++ b/tests/unit/providers/inference/test_openai_base_url_config.py @@ -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", ) diff --git a/tests/unit/server/test_replace_env_vars.py b/tests/unit/server/test_replace_env_vars.py index 14b3b7231..0c9bec860 100644 --- a/tests/unit/server/test_replace_env_vars.py +++ b/tests/unit/server/test_replace_env_vars.py @@ -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