|
6 | 6 | from pydantic import field_validator |
7 | 7 | from pydantic_settings import BaseSettings, SettingsConfigDict |
8 | 8 |
|
| 9 | +# Valid agent LLM provider prefixes for a "provider:model-name" identifier. |
| 10 | +# "ollama" runs the agent fully local via Ollama's OpenAI-compatible endpoint. |
| 11 | +VALID_MODEL_PROVIDERS: tuple[str, ...] = ( |
| 12 | + "anthropic", |
| 13 | + "openai", |
| 14 | + "google-gla", |
| 15 | + "google-vertex", |
| 16 | + "ollama", |
| 17 | +) |
| 18 | + |
| 19 | + |
| 20 | +def validate_model_identifier(v: str) -> str: |
| 21 | + """Validate an agent model identifier of the form ``provider:model-name``. |
| 22 | +
|
| 23 | + Shared by the ``Settings`` field validators and the runtime config service |
| 24 | + (``app/features/config``) so a UI-driven model change is checked the same |
| 25 | + way an env-var-driven one is. |
| 26 | +
|
| 27 | + Args: |
| 28 | + v: Model identifier string (e.g. ``anthropic:claude-sonnet-4-5``, |
| 29 | + ``ollama:llama3.1``). |
| 30 | +
|
| 31 | + Returns: |
| 32 | + The validated model identifier, unchanged. |
| 33 | +
|
| 34 | + Raises: |
| 35 | + ValueError: If the format is invalid, the model name is blank, or the |
| 36 | + provider is not in :data:`VALID_MODEL_PROVIDERS`. |
| 37 | + """ |
| 38 | + if ":" not in v: |
| 39 | + raise ValueError( |
| 40 | + f"Invalid model identifier '{v}'. " |
| 41 | + "Expected format: 'provider:model-name' " |
| 42 | + "(e.g., 'anthropic:claude-sonnet-4-5', 'ollama:llama3.1')" |
| 43 | + ) |
| 44 | + provider, model_name = v.split(":", 1) |
| 45 | + |
| 46 | + # Validate model name is non-empty and not just whitespace |
| 47 | + if not model_name or not model_name.strip(): |
| 48 | + raise ValueError( |
| 49 | + f"Invalid model identifier '{v}'. " |
| 50 | + "Model name after ':' cannot be empty or blank. " |
| 51 | + "Expected format: 'provider:model-name' " |
| 52 | + "(e.g., 'anthropic:claude-sonnet-4-5', 'ollama:llama3.1')" |
| 53 | + ) |
| 54 | + |
| 55 | + if provider not in VALID_MODEL_PROVIDERS: |
| 56 | + raise ValueError( |
| 57 | + f"Unknown provider '{provider}'. Valid providers: {list(VALID_MODEL_PROVIDERS)}" |
| 58 | + ) |
| 59 | + return v |
| 60 | + |
9 | 61 |
|
10 | 62 | class Settings(BaseSettings): |
11 | 63 | """Application settings loaded from environment variables.""" |
@@ -130,39 +182,9 @@ class Settings(BaseSettings): |
130 | 182 |
|
131 | 183 | @field_validator("agent_default_model", "agent_fallback_model") |
132 | 184 | @classmethod |
133 | | - def validate_model_identifier(cls, v: str) -> str: |
134 | | - """Validate model identifier format (provider:model-name). |
135 | | -
|
136 | | - Args: |
137 | | - v: Model identifier string. |
138 | | -
|
139 | | - Returns: |
140 | | - Validated model identifier. |
141 | | -
|
142 | | - Raises: |
143 | | - ValueError: If format is invalid or model name is missing. |
144 | | - """ |
145 | | - if ":" not in v: |
146 | | - raise ValueError( |
147 | | - f"Invalid model identifier '{v}'. " |
148 | | - "Expected format: 'provider:model-name' " |
149 | | - "(e.g., 'anthropic:claude-sonnet-4-5', 'google-gla:gemini-3-flash')" |
150 | | - ) |
151 | | - provider, model_name = v.split(":", 1) |
152 | | - |
153 | | - # Validate model name is non-empty and not just whitespace |
154 | | - if not model_name or not model_name.strip(): |
155 | | - raise ValueError( |
156 | | - f"Invalid model identifier '{v}'. " |
157 | | - "Model name after ':' cannot be empty or blank. " |
158 | | - "Expected format: 'provider:model-name' " |
159 | | - "(e.g., 'anthropic:claude-sonnet-4-5', 'google-gla:gemini-3-flash')" |
160 | | - ) |
161 | | - |
162 | | - valid_providers = ["anthropic", "openai", "google-gla", "google-vertex"] |
163 | | - if provider not in valid_providers: |
164 | | - raise ValueError(f"Unknown provider '{provider}'. Valid providers: {valid_providers}") |
165 | | - return v |
| 185 | + def _validate_agent_model(cls, v: str) -> str: |
| 186 | + """Validate agent model identifiers via :func:`validate_model_identifier`.""" |
| 187 | + return validate_model_identifier(v) |
166 | 188 |
|
167 | 189 | @property |
168 | 190 | def is_development(self) -> bool: |
|
0 commit comments