"""Settings models for ``ooai_llm``.
Purpose:
Centralize application configuration for provider credentials, default
model selection, and LangChain LLM cache behavior.
Design:
- Accept both app-prefixed and provider-native environment variables.
- Keep model defaults configurable through semantic aliases and per-provider
preset bundles.
- Expose the application directory and default cache path as computed
values derived from the working directory.
- Make model-string defaults reusable through a typed ``ModelString``.
Type aliases:
ModelPresetName: Semantic preset names available per provider.
ModelAliasName: Global semantic aliases.
Examples:
>>> settings = AppSettings()
>>> settings.resolve_model(alias="testing")
'openai:gpt-5.4-nano'
"""
from __future__ import annotations
from pathlib import Path
import os
from typing import Literal, Self
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, SecretStr, computed_field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .providers import Provider, PROVIDER_API_KEY_ENV_VARS, normalize_provider_name
from .types import ModelString
[docs]
ModelPresetName = Literal[
"default",
"latest",
"cheap",
"testing",
"fast",
"balanced",
"reasoning",
"coding",
"vision",
]
[docs]
ModelAliasName = ModelPresetName
def _load_dotenv_values() -> dict[str, str]:
"""Load non-empty values from a local ``.env`` file when available."""
try:
from dotenv import dotenv_values
except ImportError:
return {}
values = dotenv_values(".env")
return {key: str(value) for key, value in values.items() if key and value}
[docs]
class ProviderCredentials(BaseModel):
"""Provider credentials with native-env fallback aliases.
Args:
openai_api_key: OpenAI API key.
anthropic_api_key: Anthropic API key.
google_api_key: Google or Gemini API key.
xai_api_key: xAI API key.
deepseek_api_key: DeepSeek API key.
mistral_api_key: Mistral API key.
google_use_vertexai: Whether to route Gemini through Vertex AI.
google_cloud_project: Optional Google Cloud project ID.
google_cloud_location: Optional Google Cloud location.
Examples:
>>> creds = ProviderCredentials()
>>> isinstance(creds, ProviderCredentials)
True
"""
[docs]
model_config = ConfigDict(extra="forbid", populate_by_name=True)
[docs]
openai_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_OPENAI_API_KEY", "OPENAI_API_KEY"),
)
[docs]
anthropic_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"),
)
[docs]
google_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_GOOGLE_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY"),
)
[docs]
xai_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_XAI_API_KEY", "XAI_API_KEY"),
)
[docs]
deepseek_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_DEEPSEEK_API_KEY", "DEEPSEEK_API_KEY"),
)
[docs]
mistral_api_key: SecretStr | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_MISTRAL_API_KEY", "MISTRAL_API_KEY"),
)
[docs]
google_use_vertexai: bool | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_GOOGLE_USE_VERTEXAI", "GOOGLE_GENAI_USE_VERTEXAI"),
)
[docs]
google_cloud_project: str | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT"),
)
[docs]
google_cloud_location: str | None = Field(
default=None,
validation_alias=AliasChoices("OOAI_GOOGLE_CLOUD_LOCATION", "GOOGLE_CLOUD_LOCATION"),
)
@classmethod
[docs]
def from_environment(cls) -> Self:
"""Build provider credentials from native and app-scoped env vars.
Returns:
Credentials populated from environment variables.
"""
dotenv_values = _load_dotenv_values()
def first(*names: str) -> str | None:
for name in names:
value = os.environ.get(name) or dotenv_values.get(name)
if value:
return value
return None
google_vertexai = first("OOAI_GOOGLE_USE_VERTEXAI", "GOOGLE_GENAI_USE_VERTEXAI")
return cls(
openai_api_key=first("OOAI_OPENAI_API_KEY", "OPENAI_API_KEY"),
anthropic_api_key=first("OOAI_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"),
google_api_key=first("OOAI_GOOGLE_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY"),
xai_api_key=first("OOAI_XAI_API_KEY", "XAI_API_KEY"),
deepseek_api_key=first("OOAI_DEEPSEEK_API_KEY", "DEEPSEEK_API_KEY"),
mistral_api_key=first("OOAI_MISTRAL_API_KEY", "MISTRAL_API_KEY"),
google_use_vertexai=google_vertexai.lower() in {"1", "true", "yes", "on"}
if google_vertexai is not None
else None,
google_cloud_project=first("OOAI_GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT"),
google_cloud_location=first("OOAI_GOOGLE_CLOUD_LOCATION", "GOOGLE_CLOUD_LOCATION"),
)
[docs]
def get_api_key(self, provider: Provider | str) -> str | None:
"""Return the resolved API key for a provider.
Args:
provider: Canonical provider or provider alias.
Returns:
The plain API key string, or ``None`` when unavailable.
"""
resolved = normalize_provider_name(provider)
if resolved is None:
return None
mapping: dict[Provider, SecretStr | None] = {
Provider.OPENAI: self.openai_api_key,
Provider.ANTHROPIC: self.anthropic_api_key,
Provider.GOOGLE_GENAI: self.google_api_key,
Provider.XAI: self.xai_api_key,
Provider.DEEPSEEK: self.deepseek_api_key,
Provider.MISTRAL: self.mistral_api_key,
}
value = mapping[resolved]
return value.get_secret_value() if value is not None else None
[docs]
def require_api_key(self, provider: Provider | str) -> str:
"""Return the API key for a provider or raise.
Args:
provider: Canonical provider or provider alias.
Returns:
The plain API key string.
Raises:
ValueError: If no API key is configured for the provider.
"""
resolved = normalize_provider_name(provider)
if resolved is None:
raise ValueError(f"Unknown provider: {provider!r}.")
api_key = self.get_api_key(resolved)
if api_key is None:
raise ValueError(
f"No API key configured for provider {resolved.value!r}. "
f"Set {PROVIDER_API_KEY_ENV_VARS[resolved]!r} or its OOAI_ equivalent."
)
return api_key
[docs]
def to_native_environment(self) -> dict[str, str]:
"""Return provider-native environment variables.
Returns:
Mapping of native environment-variable names to string values.
"""
env: dict[str, str] = {}
if self.openai_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.OPENAI]] = self.openai_api_key.get_secret_value()
if self.anthropic_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.ANTHROPIC]] = self.anthropic_api_key.get_secret_value()
if self.google_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.GOOGLE_GENAI]] = self.google_api_key.get_secret_value()
if self.xai_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.XAI]] = self.xai_api_key.get_secret_value()
if self.deepseek_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.DEEPSEEK]] = self.deepseek_api_key.get_secret_value()
if self.mistral_api_key is not None:
env[PROVIDER_API_KEY_ENV_VARS[Provider.MISTRAL]] = self.mistral_api_key.get_secret_value()
if self.google_use_vertexai is not None:
env["GOOGLE_GENAI_USE_VERTEXAI"] = "true" if self.google_use_vertexai else "false"
if self.google_cloud_project:
env["GOOGLE_CLOUD_PROJECT"] = self.google_cloud_project
if self.google_cloud_location:
env["GOOGLE_CLOUD_LOCATION"] = self.google_cloud_location
return env
[docs]
class ProviderModelPresets(BaseModel):
"""Provider-specific model presets.
Args:
default: Recommended default model for general use.
latest: Latest recommended model when dynamic defaults are refreshed.
cheap: Lowest-cost reasonable default.
testing: Lightweight default for development and smoke tests.
fast: Lower-latency model.
balanced: Balanced cost/performance model.
reasoning: Stronger reasoning-oriented model.
coding: Coding-oriented model.
vision: Vision-capable model.
Examples:
>>> presets = ProviderModelPresets(
... default="openai:gpt-5.4-mini",
... cheap="openai:gpt-5.4-nano",
... testing="openai:gpt-5.4-nano",
... fast="openai:gpt-5.4-mini",
... balanced="openai:gpt-5.4-mini",
... reasoning="openai:gpt-5.4",
... coding="openai:gpt-5.4",
... vision="openai:gpt-5.4-mini",
... )
>>> presets.get("cheap")
'openai:gpt-5.4-nano'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
latest: str | None = None
[docs]
def get(self, preset: ModelPresetName) -> str:
"""Return the model configured for a preset.
Args:
preset: Preset name.
Returns:
Configured model string.
"""
value = getattr(self, preset)
return self.default if value is None else value
[docs]
class DefaultModelsByProvider(BaseModel):
"""Default model presets keyed by provider.
Notes:
These defaults are intentionally practical application defaults. They
are not a replacement for a live model catalog or dynamic router.
Examples:
>>> defaults = DefaultModelsByProvider()
>>> defaults.openai.cheap
'openai:gpt-5.4-nano'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
openai: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="openai:gpt-5.4-mini",
latest="openai:gpt-5.5",
cheap="openai:gpt-5.4-nano",
testing="openai:gpt-5.4-nano",
fast="openai:gpt-5.4-mini",
balanced="openai:gpt-5.4-mini",
reasoning="openai:gpt-5.4",
coding="openai:gpt-5.4",
vision="openai:gpt-5.4-mini",
)
)
[docs]
anthropic: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="anthropic:claude-sonnet-4-20250514",
cheap="anthropic:claude-3-5-haiku-20241022",
testing="anthropic:claude-3-5-haiku-20241022",
fast="anthropic:claude-3-5-haiku-20241022",
balanced="anthropic:claude-sonnet-4-20250514",
reasoning="anthropic:claude-opus-4-1-20250805",
coding="anthropic:claude-opus-4-1-20250805",
vision="anthropic:claude-sonnet-4-20250514",
)
)
[docs]
google_genai: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="google_genai:gemini-2.5-flash",
cheap="google_genai:gemini-2.5-flash-lite",
testing="google_genai:gemini-2.5-flash-lite",
fast="google_genai:gemini-2.5-flash-lite",
balanced="google_genai:gemini-2.5-flash",
reasoning="google_genai:gemini-2.5-pro",
coding="google_genai:gemini-2.5-pro",
vision="google_genai:gemini-2.5-flash",
)
)
[docs]
xai: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="xai:grok-4-1-fast-reasoning",
cheap="xai:grok-4-1-fast-non-reasoning",
testing="xai:grok-4-1-fast-non-reasoning",
fast="xai:grok-4-1-fast-non-reasoning",
balanced="xai:grok-4-1-fast-reasoning",
reasoning="xai:grok-4.20-0309-reasoning",
coding="xai:grok-4-1-fast-reasoning",
vision="xai:grok-4.20-0309-reasoning",
)
)
[docs]
deepseek: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="deepseek:deepseek-chat",
cheap="deepseek:deepseek-chat",
testing="deepseek:deepseek-chat",
fast="deepseek:deepseek-chat",
balanced="deepseek:deepseek-chat",
reasoning="deepseek:deepseek-reasoner",
coding="deepseek:deepseek-chat",
vision="deepseek:deepseek-chat",
)
)
[docs]
mistral: ProviderModelPresets = Field(
default_factory=lambda: ProviderModelPresets(
default="mistral:mistral-small-2603",
cheap="mistral:ministral-3b-2512",
testing="mistral:ministral-3b-2512",
fast="mistral:ministral-8b-2512",
balanced="mistral:mistral-small-2603",
reasoning="mistral:magistral-small-2509",
coding="mistral:devstral-2512",
vision="mistral:mistral-small-2603",
)
)
[docs]
def get(self, provider: Provider | str) -> ProviderModelPresets:
"""Return the preset bundle for a provider.
Args:
provider: Canonical provider or alias.
Returns:
Provider-specific preset bundle.
"""
normalized = normalize_provider_name(provider)
if normalized is None:
raise ValueError("Provider cannot be None.")
return getattr(self, normalized.value)
[docs]
class DefaultModelAliases(BaseModel):
"""Global semantic aliases for common model selections.
Examples:
>>> aliases = DefaultModelAliases()
>>> aliases.cheap
'openai:gpt-5.4-nano'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
default: str = "openai:gpt-5.4-mini"
[docs]
latest: str | None = "openai:gpt-5.5"
[docs]
cheap: str = "openai:gpt-5.4-nano"
[docs]
testing: str = "openai:gpt-5.4-nano"
[docs]
fast: str = "openai:gpt-5.4-mini"
[docs]
balanced: str = "openai:gpt-5.4-mini"
[docs]
reasoning: str = "openai:gpt-5.4"
[docs]
coding: str = "openai:gpt-5.4"
[docs]
vision: str = "openai:gpt-5.4-mini"
[docs]
def get(self, alias: ModelAliasName) -> str:
"""Return the model configured for an alias.
Args:
alias: Alias name.
Returns:
Configured model string.
"""
value = getattr(self, alias)
return self.default if value is None else value
[docs]
class CatalogProviderSettings(BaseModel):
"""Provider-specific model-catalog settings.
Args:
prefer_sdk: Whether SDK-based model listing should be preferred for
this provider.
limit: Optional default cap on the number of returned models.
page_size: Optional provider-native page size for paginated model
listings.
query_base: Optional Google GenAI flag controlling whether base models
are included in listings.
base_url: Optional provider base URL override for model listing.
api_version: Optional provider API version override.
Examples:
>>> cfg = CatalogProviderSettings(base_url="https://api.example.com/v1")
>>> cfg.base_url
'https://api.example.com/v1'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
prefer_sdk: bool | None = None
[docs]
limit: int | None = Field(default=None, ge=1)
[docs]
page_size: int | None = Field(default=None, ge=1)
[docs]
query_base: bool | None = None
[docs]
base_url: str | None = None
[docs]
api_version: str | None = None
[docs]
class CatalogSettings(BaseModel):
"""Settings for live model discovery and provider catalog access.
Args:
prefer_sdk: Whether SDK-based model listing should be preferred by
default.
limit: Optional default cap on the number of returned models.
page_size: Optional provider-native page size for paginated model
listings.
query_base: Optional default Google GenAI flag controlling whether base
models are included in listings.
openai: OpenAI-specific catalog settings.
anthropic: Anthropic-specific catalog settings.
google_genai: Google GenAI-specific catalog settings.
xai: xAI-specific catalog settings.
deepseek: DeepSeek-specific catalog settings.
mistral: Mistral-specific catalog settings.
Examples:
>>> catalog = CatalogSettings()
>>> catalog.xai.base_url
'https://api.x.ai'
>>> catalog.build_list_models_options('openai')['prefer_sdk']
True
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
prefer_sdk: bool = True
[docs]
limit: int | None = Field(default=None, ge=1)
[docs]
page_size: int | None = Field(default=None, ge=1)
[docs]
query_base: bool | None = None
[docs]
openai: CatalogProviderSettings = Field(default_factory=CatalogProviderSettings)
[docs]
anthropic: CatalogProviderSettings = Field(
default_factory=lambda: CatalogProviderSettings(api_version="2023-06-01")
)
[docs]
google_genai: CatalogProviderSettings = Field(default_factory=CatalogProviderSettings)
[docs]
xai: CatalogProviderSettings = Field(
default_factory=lambda: CatalogProviderSettings(base_url="https://api.x.ai")
)
[docs]
deepseek: CatalogProviderSettings = Field(
default_factory=lambda: CatalogProviderSettings(base_url="https://api.deepseek.com/v1")
)
[docs]
mistral: CatalogProviderSettings = Field(default_factory=CatalogProviderSettings)
[docs]
def get(self, provider: Provider | str) -> CatalogProviderSettings:
"""Return catalog settings for a provider.
Args:
provider: Canonical provider or provider alias.
Returns:
Provider-specific catalog settings.
"""
normalized = normalize_provider_name(provider)
if normalized is None:
raise ValueError("Provider cannot be None.")
return getattr(self, normalized.value)
[docs]
def build_list_models_options(self, provider: Provider | str) -> dict[str, object]:
"""Build default list-model options for a provider.
Args:
provider: Canonical provider or provider alias.
Returns:
Dictionary of options that can seed ``ListModelsConfig``.
"""
provider_settings = self.get(provider)
return {
"prefer_sdk": provider_settings.prefer_sdk if provider_settings.prefer_sdk is not None else self.prefer_sdk,
"limit": provider_settings.limit if provider_settings.limit is not None else self.limit,
"page_size": provider_settings.page_size if provider_settings.page_size is not None else self.page_size,
"query_base": provider_settings.query_base if provider_settings.query_base is not None else self.query_base,
}
[docs]
def build_transport_kwargs(self, provider: Provider | str) -> dict[str, str]:
"""Build provider-specific transport overrides for model discovery.
Args:
provider: Canonical provider or provider alias.
Returns:
Transport keyword arguments such as ``base_url`` or
``anthropic_version``.
"""
provider_settings = self.get(provider)
kwargs: dict[str, str] = {}
if provider_settings.base_url is not None:
kwargs["base_url"] = provider_settings.base_url
if provider_settings.api_version is not None:
kwargs["anthropic_version"] = provider_settings.api_version
return kwargs
[docs]
class LiteLLMSettings(BaseModel):
"""Native LiteLLM integration settings.
Args:
enabled: Whether native LiteLLM integration is enabled.
enrich_metadata: Whether LiteLLM pricing/model metadata should be used
to enrich LangChain-first metadata resolution.
profile_resolution_mode: How LangChain profiles and LiteLLM metadata
should be combined.
provider_prefixes: Optional per-provider LiteLLM prefix overrides.
success_callbacks: Default named LiteLLM success callbacks.
failure_callbacks: Default named LiteLLM failure callbacks.
Examples:
>>> cfg = LiteLLMSettings()
>>> cfg.provider_prefixes["google_genai"]
'gemini'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
profile_resolution_mode: Literal[
"langchain_only",
"langchain_then_litellm",
"litellm_only",
] = "langchain_then_litellm"
[docs]
provider_prefixes: dict[str, str] = Field(
default_factory=lambda: {
"openai": "openai",
"anthropic": "anthropic",
"google_genai": "gemini",
"xai": "xai",
"deepseek": "deepseek",
"mistral": "mistral",
}
)
[docs]
success_callbacks: list[str] = Field(default_factory=list)
[docs]
failure_callbacks: list[str] = Field(default_factory=list)
[docs]
class ModelDefaultsAutoRefreshSettings(BaseModel):
"""Factory-time model-default refresh settings.
Args:
enabled: Whether factories should refresh model defaults before model
resolution.
source: Refresh source. ``"auto"`` uses provider catalogs when a
credential is configured and falls back to LiteLLM metadata.
providers: Optional provider list. Defaults to every supported provider.
primary_alias_provider: Provider whose refreshed presets update global
aliases such as ``alias="latest"``.
strict: Whether refresh failures should raise instead of preserving
existing defaults.
cache_seconds: Process-local TTL for automatic refresh results. Set to
``0`` to refresh on every factory call.
Examples:
>>> cfg = ModelDefaultsAutoRefreshSettings(enabled=True, providers=["openai"])
>>> cfg.source
'auto'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
source: Literal["auto", "provider", "litellm"] = "auto"
[docs]
providers: list[str] | None = None
[docs]
primary_alias_provider: str = "openai"
[docs]
cache_seconds: int | None = Field(default=3600, ge=0)
[docs]
class LLMCacheSettings(BaseModel):
"""LLM cache configuration.
Args:
enabled: Whether global LLM caching should be enabled by default.
backend: Cache backend identifier.
path: Optional explicit cache path.
filename: Cache filename when ``path`` is not explicitly provided.
create_dirs: Whether parent directories should be created automatically.
ttl: Optional time-to-live in seconds for backends that support it.
redis_url: Redis connection URL for the ``redis`` backend.
redis_host: Redis host when ``redis_url`` is not supplied.
redis_port: Redis port when ``redis_url`` is not supplied.
redis_db: Redis database index when ``redis_url`` is not supplied.
redis_username: Redis username when ``redis_url`` is not supplied.
redis_password: Redis password when ``redis_url`` is not supplied.
redis_ssl: Whether Redis should use TLS when ``redis_url`` is absent.
redis_connection_kwargs: Additional Redis client constructor kwargs.
upstash_url: Upstash Redis REST URL.
upstash_token: Upstash Redis REST token.
sqlalchemy_url: SQLAlchemy database URL for the ``sqlalchemy`` backend.
Examples:
>>> cache = LLMCacheSettings()
>>> cache.backend
'sqlite'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
backend: str = "sqlite"
[docs]
path: Path | None = None
[docs]
filename: str = "langchain_llm_cache.sqlite3"
[docs]
create_dirs: bool = True
[docs]
ttl: int | None = Field(default=None, ge=1)
[docs]
redis_url: str | None = None
[docs]
redis_host: str = "localhost"
[docs]
redis_port: int = Field(default=6379, ge=1, le=65535)
[docs]
redis_db: int = Field(default=0, ge=0)
[docs]
redis_username: str | None = None
[docs]
redis_password: SecretStr | None = None
[docs]
redis_ssl: bool = False
[docs]
redis_connection_kwargs: dict[str, object] = Field(default_factory=dict)
[docs]
upstash_url: str | None = None
[docs]
upstash_token: SecretStr | None = None
[docs]
sqlalchemy_url: str | None = None
[docs]
class LLMSettings(BaseModel):
"""Top-level LLM settings.
Args:
default_model: Fallback model when no alias or provider default is used.
defaults_by_provider: Default model preset bundles per provider.
aliases: Global semantic aliases.
auto_refresh_models: Factory-time model-default refresh settings.
cache: Global cache settings.
Examples:
>>> llm = LLMSettings()
>>> llm.default_model
'openai:gpt-5.4-mini'
"""
[docs]
model_config = ConfigDict(extra="forbid")
[docs]
default_model: str = "openai:gpt-5.4-mini"
[docs]
defaults_by_provider: DefaultModelsByProvider = Field(default_factory=DefaultModelsByProvider)
[docs]
aliases: DefaultModelAliases = Field(default_factory=DefaultModelAliases)
[docs]
auto_refresh_models: ModelDefaultsAutoRefreshSettings = Field(
default_factory=ModelDefaultsAutoRefreshSettings
)
[docs]
cache: LLMCacheSettings = Field(default_factory=LLMCacheSettings)
[docs]
class AppSettings(BaseSettings):
"""Application settings for the LLM layer.
Args:
app_name: Human-readable application name.
app_root: Root directory used for derived paths.
app_dir_name: Hidden directory rooted under ``app_root`` for local
cache and application files.
credentials: Provider credential settings.
llm: LLM settings.
catalog: Live model-discovery settings.
litellm: Native LiteLLM integration settings.
Examples:
>>> settings = AppSettings()
>>> settings.resolve_model(alias="reasoning")
'openai:gpt-5.4'
>>> settings.catalog.xai.base_url
'https://api.x.ai'
>>> settings.litellm.profile_resolution_mode
'langchain_then_litellm'
"""
[docs]
model_config = SettingsConfigDict(
env_prefix="OOAI_",
env_nested_delimiter="__",
env_file=".env",
env_file_encoding="utf-8",
env_ignore_empty=True,
extra="ignore",
)
@field_validator("credentials", mode="before")
@classmethod
def _coerce_credentials(cls, value: ProviderCredentials | dict[str, object]) -> ProviderCredentials | dict[str, object]:
"""Coerce nested credential dictionaries into typed credentials.
Args:
value: Incoming credentials value.
Returns:
Typed credentials object or untouched value.
"""
if isinstance(value, dict):
return ProviderCredentials.model_validate(value)
return value
[docs]
app_root: Path = Field(default_factory=Path.cwd)
[docs]
app_dir_name: str = ".ooai"
[docs]
credentials: ProviderCredentials = Field(default_factory=ProviderCredentials.from_environment)
[docs]
llm: LLMSettings = Field(default_factory=LLMSettings)
[docs]
catalog: CatalogSettings = Field(default_factory=CatalogSettings)
[docs]
litellm: LiteLLMSettings = Field(default_factory=LiteLLMSettings)
@computed_field # type: ignore[prop-decorator]
@property
[docs]
def app_dir(self) -> Path:
"""Return the resolved application directory.
Returns:
Resolved application directory.
"""
return (self.app_root / self.app_dir_name).resolve()
@computed_field # type: ignore[prop-decorator]
@property
[docs]
def default_llm_cache_path(self) -> Path:
"""Return the resolved default LLM cache path.
Returns:
Effective cache file path.
"""
explicit_path = self.llm.cache.path
if explicit_path is not None:
return explicit_path.expanduser().resolve()
return (self.app_dir / "cache" / "llm" / self.llm.cache.filename).resolve()
@computed_field # type: ignore[prop-decorator]
@property
[docs]
def default_model_string(self) -> ModelString:
"""Return the configured top-level default as a typed model string.
Returns:
Typed default model string.
"""
return ModelString.parse(self.llm.default_model)
[docs]
def model_copy_with_root(self, app_root: Path) -> Self:
"""Return a copy of the settings with a new application root.
Args:
app_root: New application root.
Returns:
Updated settings copy.
"""
return self.model_copy(update={"app_root": app_root})
[docs]
def resolve_model(
self,
*,
model: str | None = None,
alias: ModelAliasName | None = None,
provider: Provider | str | None = None,
preset: ModelPresetName = "default",
) -> str:
"""Resolve the effective model string.
Resolution order:
1. Explicit ``model``.
2. Global semantic ``alias``.
3. Provider-specific ``preset``.
4. Global fallback ``llm.default_model``.
Args:
model: Explicit model string.
alias: Optional semantic alias.
provider: Optional provider name or alias.
preset: Provider-specific preset name.
Returns:
Resolved model string.
Raises:
ValueError: If an alias and provider are both supplied.
"""
if model is not None:
return model
if alias is not None and provider is not None:
raise ValueError("Pass either alias or provider, not both.")
if alias is not None:
return self.llm.aliases.get(alias)
if provider is not None:
return self.llm.defaults_by_provider.get(provider).get(preset)
return self.llm.default_model
[docs]
def resolve_model_string(
self,
*,
model: str | ModelString | None = None,
alias: ModelAliasName | None = None,
provider: Provider | str | None = None,
preset: ModelPresetName = "default",
) -> ModelString:
"""Resolve the effective model as a typed model string.
Args:
model: Explicit model string or typed model string.
alias: Optional semantic alias.
provider: Optional provider name or alias.
preset: Provider-specific preset name.
Returns:
Typed model string.
"""
if isinstance(model, ModelString):
return model
return ModelString.parse(
self.resolve_model(model=model, alias=alias, provider=provider, preset=preset)
)