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

@ -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(