kiln_ai.utils.config
1import copy 2import getpass 3import os 4import threading 5from pathlib import Path 6from typing import Any, Callable, Dict, List, Optional 7 8import yaml 9 10# Configuration keys 11MCP_SECRETS_KEY = "mcp_secrets" 12 13 14class ConfigProperty: 15 def __init__( 16 self, 17 type_: type, 18 default: Any = None, 19 env_var: Optional[str] = None, 20 default_lambda: Optional[Callable[[], Any]] = None, 21 sensitive: bool = False, 22 sensitive_keys: Optional[List[str]] = None, 23 in_memory: bool = False, 24 ): 25 self.type = type_ 26 self.default = default 27 self.env_var = env_var 28 self.default_lambda = default_lambda 29 self.sensitive = sensitive 30 self.sensitive_keys = sensitive_keys 31 self.in_memory = in_memory 32 33 34class Config: 35 _shared_instance = None 36 37 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 38 self._properties: Dict[str, ConfigProperty] = properties or { 39 "user_id": ConfigProperty( 40 str, 41 env_var="KILN_USER_ID", 42 default_lambda=_get_user_id, 43 ), 44 "autosave_runs": ConfigProperty( 45 bool, 46 env_var="KILN_AUTOSAVE_RUNS", 47 default=True, 48 in_memory=True, 49 ), 50 "open_ai_api_key": ConfigProperty( 51 str, 52 env_var="OPENAI_API_KEY", 53 sensitive=True, 54 ), 55 "groq_api_key": ConfigProperty( 56 str, 57 env_var="GROQ_API_KEY", 58 sensitive=True, 59 ), 60 "ollama_base_url": ConfigProperty( 61 str, 62 env_var="OLLAMA_BASE_URL", 63 ), 64 "docker_model_runner_base_url": ConfigProperty( 65 str, 66 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 67 ), 68 "bedrock_access_key": ConfigProperty( 69 str, 70 env_var="AWS_ACCESS_KEY_ID", 71 sensitive=True, 72 ), 73 "bedrock_secret_key": ConfigProperty( 74 str, 75 env_var="AWS_SECRET_ACCESS_KEY", 76 sensitive=True, 77 ), 78 "open_router_api_key": ConfigProperty( 79 str, 80 env_var="OPENROUTER_API_KEY", 81 sensitive=True, 82 ), 83 "fireworks_api_key": ConfigProperty( 84 str, 85 env_var="FIREWORKS_API_KEY", 86 sensitive=True, 87 ), 88 "fireworks_account_id": ConfigProperty( 89 str, 90 env_var="FIREWORKS_ACCOUNT_ID", 91 ), 92 "anthropic_api_key": ConfigProperty( 93 str, 94 env_var="ANTHROPIC_API_KEY", 95 sensitive=True, 96 ), 97 "gemini_api_key": ConfigProperty( 98 str, 99 env_var="GEMINI_API_KEY", 100 sensitive=True, 101 ), 102 "projects": ConfigProperty( 103 list, 104 default_lambda=lambda: [], 105 ), 106 "azure_openai_api_key": ConfigProperty( 107 str, 108 env_var="AZURE_OPENAI_API_KEY", 109 sensitive=True, 110 ), 111 "azure_openai_endpoint": ConfigProperty( 112 str, 113 env_var="AZURE_OPENAI_ENDPOINT", 114 ), 115 "huggingface_api_key": ConfigProperty( 116 str, 117 env_var="HUGGINGFACE_API_KEY", 118 sensitive=True, 119 ), 120 "vertex_project_id": ConfigProperty( 121 str, 122 env_var="VERTEX_PROJECT_ID", 123 ), 124 "vertex_location": ConfigProperty( 125 str, 126 env_var="VERTEX_LOCATION", 127 ), 128 "together_api_key": ConfigProperty( 129 str, 130 env_var="TOGETHERAI_API_KEY", 131 sensitive=True, 132 ), 133 "wandb_api_key": ConfigProperty( 134 str, 135 env_var="WANDB_API_KEY", 136 sensitive=True, 137 ), 138 "wandb_entity": ConfigProperty( 139 str, 140 env_var="WANDB_ENTITY", 141 ), 142 "siliconflow_cn_api_key": ConfigProperty( 143 str, 144 env_var="SILICONFLOW_CN_API_KEY", 145 sensitive=True, 146 ), 147 "wandb_base_url": ConfigProperty( 148 str, 149 env_var="WANDB_BASE_URL", 150 ), 151 # Legacy custom models, replaced by user_model_registry below 152 "custom_models": ConfigProperty( 153 list, 154 default_lambda=lambda: [], 155 ), 156 "user_model_registry": ConfigProperty( 157 list, 158 default_lambda=lambda: [], 159 ), 160 "openai_compatible_providers": ConfigProperty( 161 list, 162 default_lambda=lambda: [], 163 sensitive_keys=["api_key"], 164 ), 165 "cerebras_api_key": ConfigProperty( 166 str, 167 env_var="CEREBRAS_API_KEY", 168 sensitive=True, 169 ), 170 "kiln_copilot_api_key": ConfigProperty( 171 str, 172 env_var="KILN_COPILOT_API_KEY", 173 sensitive=True, 174 ), 175 "enable_demo_tools": ConfigProperty( 176 bool, 177 env_var="ENABLE_DEMO_TOOLS", 178 default=False, 179 ), 180 # Allow the user to set the path to lookup MCP server commands, like npx. 181 "custom_mcp_path": ConfigProperty( 182 str, 183 env_var="CUSTOM_MCP_PATH", 184 ), 185 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 186 MCP_SECRETS_KEY: ConfigProperty( 187 dict[str, str], 188 sensitive=True, 189 ), 190 # has the user indicated it's for personal or work use? 191 "user_type": ConfigProperty( 192 str, # "personal" or "work" 193 ), 194 # if the user has provided their work contact 195 "work_use_contact": ConfigProperty( 196 str, 197 ), 198 # if the user has provided their personal contact 199 "personal_use_contact": ConfigProperty( 200 str, 201 ), 202 } 203 self._lock = threading.Lock() 204 self._in_memory_settings: Dict[str, Any] = {} 205 self._settings = self.load_settings() 206 207 @classmethod 208 def shared(cls): 209 if cls._shared_instance is None: 210 cls._shared_instance = cls() 211 return cls._shared_instance 212 213 # Get a value, mockable for testing 214 def get_value(self, name: str) -> Any: 215 try: 216 return self.__getattr__(name) 217 except AttributeError: 218 return None 219 220 def __getattr__(self, name: str) -> Any: 221 if name == "_properties": 222 return super().__getattribute__("_properties") 223 if name not in self._properties: 224 return super().__getattribute__(name) 225 226 property_config = self._properties[name] 227 228 if property_config.in_memory: 229 if name in self._in_memory_settings: 230 value = self._in_memory_settings[name] 231 return value if value is None else property_config.type(value) 232 else: 233 if name in self._settings: 234 value = self._settings[name] 235 return value if value is None else property_config.type(value) 236 237 # Check environment variable 238 if property_config.env_var and property_config.env_var in os.environ: 239 value = os.environ[property_config.env_var] 240 return property_config.type(value) 241 242 # Use default value or default_lambda 243 if property_config.default_lambda: 244 value = property_config.default_lambda() 245 else: 246 value = property_config.default 247 248 return None if value is None else property_config.type(value) 249 250 def __setattr__(self, name, value): 251 if name in ("_properties", "_settings", "_lock", "_in_memory_settings"): 252 super().__setattr__(name, value) 253 elif name in self._properties: 254 if self._properties[name].in_memory: 255 with self._lock: 256 self._in_memory_settings[name] = value 257 else: 258 self.update_settings({name: value}) 259 else: 260 raise AttributeError(f"Config has no attribute '{name}'") 261 262 @classmethod 263 def settings_dir(cls, create=True) -> str: 264 settings_dir = os.path.join(Path.home(), ".kiln_ai") 265 if create and not os.path.exists(settings_dir): 266 os.makedirs(settings_dir) 267 return settings_dir 268 269 @classmethod 270 def settings_path(cls, create=True) -> str: 271 settings_dir = cls.settings_dir(create) 272 return os.path.join(settings_dir, "settings.yaml") 273 274 @classmethod 275 def load_settings(cls): 276 if not os.path.isfile(cls.settings_path(create=False)): 277 return {} 278 with open(cls.settings_path(), "r") as f: 279 settings = yaml.safe_load(f.read()) or {} 280 return settings 281 282 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 283 with self._lock: 284 filtered_disk = { 285 k: v 286 for k, v in self._settings.items() 287 if k not in self._properties or not self._properties[k].in_memory 288 } 289 combined = {**filtered_disk, **self._in_memory_settings} 290 291 if not hide_sensitive: 292 return combined 293 294 settings = { 295 k: "[hidden]" 296 if k in self._properties and self._properties[k].sensitive 297 else copy.deepcopy(v) 298 for k, v in combined.items() 299 } 300 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 301 for key, value in settings.items(): 302 if key in self._properties and self._properties[key].sensitive_keys: 303 sensitive_keys = self._properties[key].sensitive_keys or [] 304 for sensitive_key in sensitive_keys: 305 if isinstance(value, list): 306 for item in value: 307 if sensitive_key in item: 308 item[sensitive_key] = "[hidden]" 309 310 return settings 311 312 def save_setting(self, name: str, value: Any): 313 self.update_settings({name: value}) 314 315 def update_settings(self, new_settings: Dict[str, Any]): 316 with self._lock: 317 in_memory_updates = { 318 k: v 319 for k, v in new_settings.items() 320 if k in self._properties and self._properties[k].in_memory 321 } 322 disk_updates = { 323 k: v 324 for k, v in new_settings.items() 325 if k not in self._properties or not self._properties[k].in_memory 326 } 327 328 if in_memory_updates: 329 self._in_memory_settings.update(in_memory_updates) 330 331 if disk_updates: 332 # Fresh load to avoid clobbering changes from other instances 333 current_settings = self.load_settings() 334 current_settings.update(disk_updates) 335 # remove None values 336 current_settings = { 337 k: v for k, v in current_settings.items() if v is not None 338 } 339 with open(self.settings_path(), "w") as f: 340 yaml.dump(current_settings, f) 341 self._settings = current_settings 342 343 344def _get_user_id(): 345 try: 346 return getpass.getuser() or "unknown_user" 347 except Exception: 348 return "unknown_user"
MCP_SECRETS_KEY =
'mcp_secrets'
class
ConfigProperty:
15class ConfigProperty: 16 def __init__( 17 self, 18 type_: type, 19 default: Any = None, 20 env_var: Optional[str] = None, 21 default_lambda: Optional[Callable[[], Any]] = None, 22 sensitive: bool = False, 23 sensitive_keys: Optional[List[str]] = None, 24 in_memory: bool = False, 25 ): 26 self.type = type_ 27 self.default = default 28 self.env_var = env_var 29 self.default_lambda = default_lambda 30 self.sensitive = sensitive 31 self.sensitive_keys = sensitive_keys 32 self.in_memory = in_memory
ConfigProperty( type_: type, default: Any = None, env_var: Optional[str] = None, default_lambda: Optional[Callable[[], Any]] = None, sensitive: bool = False, sensitive_keys: Optional[List[str]] = None, in_memory: bool = False)
16 def __init__( 17 self, 18 type_: type, 19 default: Any = None, 20 env_var: Optional[str] = None, 21 default_lambda: Optional[Callable[[], Any]] = None, 22 sensitive: bool = False, 23 sensitive_keys: Optional[List[str]] = None, 24 in_memory: bool = False, 25 ): 26 self.type = type_ 27 self.default = default 28 self.env_var = env_var 29 self.default_lambda = default_lambda 30 self.sensitive = sensitive 31 self.sensitive_keys = sensitive_keys 32 self.in_memory = in_memory
class
Config:
35class Config: 36 _shared_instance = None 37 38 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 39 self._properties: Dict[str, ConfigProperty] = properties or { 40 "user_id": ConfigProperty( 41 str, 42 env_var="KILN_USER_ID", 43 default_lambda=_get_user_id, 44 ), 45 "autosave_runs": ConfigProperty( 46 bool, 47 env_var="KILN_AUTOSAVE_RUNS", 48 default=True, 49 in_memory=True, 50 ), 51 "open_ai_api_key": ConfigProperty( 52 str, 53 env_var="OPENAI_API_KEY", 54 sensitive=True, 55 ), 56 "groq_api_key": ConfigProperty( 57 str, 58 env_var="GROQ_API_KEY", 59 sensitive=True, 60 ), 61 "ollama_base_url": ConfigProperty( 62 str, 63 env_var="OLLAMA_BASE_URL", 64 ), 65 "docker_model_runner_base_url": ConfigProperty( 66 str, 67 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 68 ), 69 "bedrock_access_key": ConfigProperty( 70 str, 71 env_var="AWS_ACCESS_KEY_ID", 72 sensitive=True, 73 ), 74 "bedrock_secret_key": ConfigProperty( 75 str, 76 env_var="AWS_SECRET_ACCESS_KEY", 77 sensitive=True, 78 ), 79 "open_router_api_key": ConfigProperty( 80 str, 81 env_var="OPENROUTER_API_KEY", 82 sensitive=True, 83 ), 84 "fireworks_api_key": ConfigProperty( 85 str, 86 env_var="FIREWORKS_API_KEY", 87 sensitive=True, 88 ), 89 "fireworks_account_id": ConfigProperty( 90 str, 91 env_var="FIREWORKS_ACCOUNT_ID", 92 ), 93 "anthropic_api_key": ConfigProperty( 94 str, 95 env_var="ANTHROPIC_API_KEY", 96 sensitive=True, 97 ), 98 "gemini_api_key": ConfigProperty( 99 str, 100 env_var="GEMINI_API_KEY", 101 sensitive=True, 102 ), 103 "projects": ConfigProperty( 104 list, 105 default_lambda=lambda: [], 106 ), 107 "azure_openai_api_key": ConfigProperty( 108 str, 109 env_var="AZURE_OPENAI_API_KEY", 110 sensitive=True, 111 ), 112 "azure_openai_endpoint": ConfigProperty( 113 str, 114 env_var="AZURE_OPENAI_ENDPOINT", 115 ), 116 "huggingface_api_key": ConfigProperty( 117 str, 118 env_var="HUGGINGFACE_API_KEY", 119 sensitive=True, 120 ), 121 "vertex_project_id": ConfigProperty( 122 str, 123 env_var="VERTEX_PROJECT_ID", 124 ), 125 "vertex_location": ConfigProperty( 126 str, 127 env_var="VERTEX_LOCATION", 128 ), 129 "together_api_key": ConfigProperty( 130 str, 131 env_var="TOGETHERAI_API_KEY", 132 sensitive=True, 133 ), 134 "wandb_api_key": ConfigProperty( 135 str, 136 env_var="WANDB_API_KEY", 137 sensitive=True, 138 ), 139 "wandb_entity": ConfigProperty( 140 str, 141 env_var="WANDB_ENTITY", 142 ), 143 "siliconflow_cn_api_key": ConfigProperty( 144 str, 145 env_var="SILICONFLOW_CN_API_KEY", 146 sensitive=True, 147 ), 148 "wandb_base_url": ConfigProperty( 149 str, 150 env_var="WANDB_BASE_URL", 151 ), 152 # Legacy custom models, replaced by user_model_registry below 153 "custom_models": ConfigProperty( 154 list, 155 default_lambda=lambda: [], 156 ), 157 "user_model_registry": ConfigProperty( 158 list, 159 default_lambda=lambda: [], 160 ), 161 "openai_compatible_providers": ConfigProperty( 162 list, 163 default_lambda=lambda: [], 164 sensitive_keys=["api_key"], 165 ), 166 "cerebras_api_key": ConfigProperty( 167 str, 168 env_var="CEREBRAS_API_KEY", 169 sensitive=True, 170 ), 171 "kiln_copilot_api_key": ConfigProperty( 172 str, 173 env_var="KILN_COPILOT_API_KEY", 174 sensitive=True, 175 ), 176 "enable_demo_tools": ConfigProperty( 177 bool, 178 env_var="ENABLE_DEMO_TOOLS", 179 default=False, 180 ), 181 # Allow the user to set the path to lookup MCP server commands, like npx. 182 "custom_mcp_path": ConfigProperty( 183 str, 184 env_var="CUSTOM_MCP_PATH", 185 ), 186 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 187 MCP_SECRETS_KEY: ConfigProperty( 188 dict[str, str], 189 sensitive=True, 190 ), 191 # has the user indicated it's for personal or work use? 192 "user_type": ConfigProperty( 193 str, # "personal" or "work" 194 ), 195 # if the user has provided their work contact 196 "work_use_contact": ConfigProperty( 197 str, 198 ), 199 # if the user has provided their personal contact 200 "personal_use_contact": ConfigProperty( 201 str, 202 ), 203 } 204 self._lock = threading.Lock() 205 self._in_memory_settings: Dict[str, Any] = {} 206 self._settings = self.load_settings() 207 208 @classmethod 209 def shared(cls): 210 if cls._shared_instance is None: 211 cls._shared_instance = cls() 212 return cls._shared_instance 213 214 # Get a value, mockable for testing 215 def get_value(self, name: str) -> Any: 216 try: 217 return self.__getattr__(name) 218 except AttributeError: 219 return None 220 221 def __getattr__(self, name: str) -> Any: 222 if name == "_properties": 223 return super().__getattribute__("_properties") 224 if name not in self._properties: 225 return super().__getattribute__(name) 226 227 property_config = self._properties[name] 228 229 if property_config.in_memory: 230 if name in self._in_memory_settings: 231 value = self._in_memory_settings[name] 232 return value if value is None else property_config.type(value) 233 else: 234 if name in self._settings: 235 value = self._settings[name] 236 return value if value is None else property_config.type(value) 237 238 # Check environment variable 239 if property_config.env_var and property_config.env_var in os.environ: 240 value = os.environ[property_config.env_var] 241 return property_config.type(value) 242 243 # Use default value or default_lambda 244 if property_config.default_lambda: 245 value = property_config.default_lambda() 246 else: 247 value = property_config.default 248 249 return None if value is None else property_config.type(value) 250 251 def __setattr__(self, name, value): 252 if name in ("_properties", "_settings", "_lock", "_in_memory_settings"): 253 super().__setattr__(name, value) 254 elif name in self._properties: 255 if self._properties[name].in_memory: 256 with self._lock: 257 self._in_memory_settings[name] = value 258 else: 259 self.update_settings({name: value}) 260 else: 261 raise AttributeError(f"Config has no attribute '{name}'") 262 263 @classmethod 264 def settings_dir(cls, create=True) -> str: 265 settings_dir = os.path.join(Path.home(), ".kiln_ai") 266 if create and not os.path.exists(settings_dir): 267 os.makedirs(settings_dir) 268 return settings_dir 269 270 @classmethod 271 def settings_path(cls, create=True) -> str: 272 settings_dir = cls.settings_dir(create) 273 return os.path.join(settings_dir, "settings.yaml") 274 275 @classmethod 276 def load_settings(cls): 277 if not os.path.isfile(cls.settings_path(create=False)): 278 return {} 279 with open(cls.settings_path(), "r") as f: 280 settings = yaml.safe_load(f.read()) or {} 281 return settings 282 283 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 284 with self._lock: 285 filtered_disk = { 286 k: v 287 for k, v in self._settings.items() 288 if k not in self._properties or not self._properties[k].in_memory 289 } 290 combined = {**filtered_disk, **self._in_memory_settings} 291 292 if not hide_sensitive: 293 return combined 294 295 settings = { 296 k: "[hidden]" 297 if k in self._properties and self._properties[k].sensitive 298 else copy.deepcopy(v) 299 for k, v in combined.items() 300 } 301 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 302 for key, value in settings.items(): 303 if key in self._properties and self._properties[key].sensitive_keys: 304 sensitive_keys = self._properties[key].sensitive_keys or [] 305 for sensitive_key in sensitive_keys: 306 if isinstance(value, list): 307 for item in value: 308 if sensitive_key in item: 309 item[sensitive_key] = "[hidden]" 310 311 return settings 312 313 def save_setting(self, name: str, value: Any): 314 self.update_settings({name: value}) 315 316 def update_settings(self, new_settings: Dict[str, Any]): 317 with self._lock: 318 in_memory_updates = { 319 k: v 320 for k, v in new_settings.items() 321 if k in self._properties and self._properties[k].in_memory 322 } 323 disk_updates = { 324 k: v 325 for k, v in new_settings.items() 326 if k not in self._properties or not self._properties[k].in_memory 327 } 328 329 if in_memory_updates: 330 self._in_memory_settings.update(in_memory_updates) 331 332 if disk_updates: 333 # Fresh load to avoid clobbering changes from other instances 334 current_settings = self.load_settings() 335 current_settings.update(disk_updates) 336 # remove None values 337 current_settings = { 338 k: v for k, v in current_settings.items() if v is not None 339 } 340 with open(self.settings_path(), "w") as f: 341 yaml.dump(current_settings, f) 342 self._settings = current_settings
Config( properties: Optional[Dict[str, ConfigProperty]] = None)
38 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 39 self._properties: Dict[str, ConfigProperty] = properties or { 40 "user_id": ConfigProperty( 41 str, 42 env_var="KILN_USER_ID", 43 default_lambda=_get_user_id, 44 ), 45 "autosave_runs": ConfigProperty( 46 bool, 47 env_var="KILN_AUTOSAVE_RUNS", 48 default=True, 49 in_memory=True, 50 ), 51 "open_ai_api_key": ConfigProperty( 52 str, 53 env_var="OPENAI_API_KEY", 54 sensitive=True, 55 ), 56 "groq_api_key": ConfigProperty( 57 str, 58 env_var="GROQ_API_KEY", 59 sensitive=True, 60 ), 61 "ollama_base_url": ConfigProperty( 62 str, 63 env_var="OLLAMA_BASE_URL", 64 ), 65 "docker_model_runner_base_url": ConfigProperty( 66 str, 67 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 68 ), 69 "bedrock_access_key": ConfigProperty( 70 str, 71 env_var="AWS_ACCESS_KEY_ID", 72 sensitive=True, 73 ), 74 "bedrock_secret_key": ConfigProperty( 75 str, 76 env_var="AWS_SECRET_ACCESS_KEY", 77 sensitive=True, 78 ), 79 "open_router_api_key": ConfigProperty( 80 str, 81 env_var="OPENROUTER_API_KEY", 82 sensitive=True, 83 ), 84 "fireworks_api_key": ConfigProperty( 85 str, 86 env_var="FIREWORKS_API_KEY", 87 sensitive=True, 88 ), 89 "fireworks_account_id": ConfigProperty( 90 str, 91 env_var="FIREWORKS_ACCOUNT_ID", 92 ), 93 "anthropic_api_key": ConfigProperty( 94 str, 95 env_var="ANTHROPIC_API_KEY", 96 sensitive=True, 97 ), 98 "gemini_api_key": ConfigProperty( 99 str, 100 env_var="GEMINI_API_KEY", 101 sensitive=True, 102 ), 103 "projects": ConfigProperty( 104 list, 105 default_lambda=lambda: [], 106 ), 107 "azure_openai_api_key": ConfigProperty( 108 str, 109 env_var="AZURE_OPENAI_API_KEY", 110 sensitive=True, 111 ), 112 "azure_openai_endpoint": ConfigProperty( 113 str, 114 env_var="AZURE_OPENAI_ENDPOINT", 115 ), 116 "huggingface_api_key": ConfigProperty( 117 str, 118 env_var="HUGGINGFACE_API_KEY", 119 sensitive=True, 120 ), 121 "vertex_project_id": ConfigProperty( 122 str, 123 env_var="VERTEX_PROJECT_ID", 124 ), 125 "vertex_location": ConfigProperty( 126 str, 127 env_var="VERTEX_LOCATION", 128 ), 129 "together_api_key": ConfigProperty( 130 str, 131 env_var="TOGETHERAI_API_KEY", 132 sensitive=True, 133 ), 134 "wandb_api_key": ConfigProperty( 135 str, 136 env_var="WANDB_API_KEY", 137 sensitive=True, 138 ), 139 "wandb_entity": ConfigProperty( 140 str, 141 env_var="WANDB_ENTITY", 142 ), 143 "siliconflow_cn_api_key": ConfigProperty( 144 str, 145 env_var="SILICONFLOW_CN_API_KEY", 146 sensitive=True, 147 ), 148 "wandb_base_url": ConfigProperty( 149 str, 150 env_var="WANDB_BASE_URL", 151 ), 152 # Legacy custom models, replaced by user_model_registry below 153 "custom_models": ConfigProperty( 154 list, 155 default_lambda=lambda: [], 156 ), 157 "user_model_registry": ConfigProperty( 158 list, 159 default_lambda=lambda: [], 160 ), 161 "openai_compatible_providers": ConfigProperty( 162 list, 163 default_lambda=lambda: [], 164 sensitive_keys=["api_key"], 165 ), 166 "cerebras_api_key": ConfigProperty( 167 str, 168 env_var="CEREBRAS_API_KEY", 169 sensitive=True, 170 ), 171 "kiln_copilot_api_key": ConfigProperty( 172 str, 173 env_var="KILN_COPILOT_API_KEY", 174 sensitive=True, 175 ), 176 "enable_demo_tools": ConfigProperty( 177 bool, 178 env_var="ENABLE_DEMO_TOOLS", 179 default=False, 180 ), 181 # Allow the user to set the path to lookup MCP server commands, like npx. 182 "custom_mcp_path": ConfigProperty( 183 str, 184 env_var="CUSTOM_MCP_PATH", 185 ), 186 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 187 MCP_SECRETS_KEY: ConfigProperty( 188 dict[str, str], 189 sensitive=True, 190 ), 191 # has the user indicated it's for personal or work use? 192 "user_type": ConfigProperty( 193 str, # "personal" or "work" 194 ), 195 # if the user has provided their work contact 196 "work_use_contact": ConfigProperty( 197 str, 198 ), 199 # if the user has provided their personal contact 200 "personal_use_contact": ConfigProperty( 201 str, 202 ), 203 } 204 self._lock = threading.Lock() 205 self._in_memory_settings: Dict[str, Any] = {} 206 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
283 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 284 with self._lock: 285 filtered_disk = { 286 k: v 287 for k, v in self._settings.items() 288 if k not in self._properties or not self._properties[k].in_memory 289 } 290 combined = {**filtered_disk, **self._in_memory_settings} 291 292 if not hide_sensitive: 293 return combined 294 295 settings = { 296 k: "[hidden]" 297 if k in self._properties and self._properties[k].sensitive 298 else copy.deepcopy(v) 299 for k, v in combined.items() 300 } 301 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 302 for key, value in settings.items(): 303 if key in self._properties and self._properties[key].sensitive_keys: 304 sensitive_keys = self._properties[key].sensitive_keys or [] 305 for sensitive_key in sensitive_keys: 306 if isinstance(value, list): 307 for item in value: 308 if sensitive_key in item: 309 item[sensitive_key] = "[hidden]" 310 311 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
316 def update_settings(self, new_settings: Dict[str, Any]): 317 with self._lock: 318 in_memory_updates = { 319 k: v 320 for k, v in new_settings.items() 321 if k in self._properties and self._properties[k].in_memory 322 } 323 disk_updates = { 324 k: v 325 for k, v in new_settings.items() 326 if k not in self._properties or not self._properties[k].in_memory 327 } 328 329 if in_memory_updates: 330 self._in_memory_settings.update(in_memory_updates) 331 332 if disk_updates: 333 # Fresh load to avoid clobbering changes from other instances 334 current_settings = self.load_settings() 335 current_settings.update(disk_updates) 336 # remove None values 337 current_settings = { 338 k: v for k, v in current_settings.items() if v is not None 339 } 340 with open(self.settings_path(), "w") as f: 341 yaml.dump(current_settings, f) 342 self._settings = current_settings