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 "featherless_ai_api_key": ConfigProperty( 171 str, 172 env_var="FEATHERLESS_AI_API_KEY", 173 sensitive=True, 174 ), 175 "kiln_copilot_api_key": ConfigProperty( 176 str, 177 env_var="KILN_COPILOT_API_KEY", 178 sensitive=True, 179 ), 180 "enable_demo_tools": ConfigProperty( 181 bool, 182 env_var="ENABLE_DEMO_TOOLS", 183 default=False, 184 ), 185 "kiln_local_api_host": ConfigProperty( 186 str, 187 env_var="KILN_LOCAL_API_HOST", 188 default="127.0.0.1", 189 in_memory=True, 190 ), 191 "kiln_local_api_port": ConfigProperty( 192 int, 193 env_var="KILN_LOCAL_API_PORT", 194 default=8757, 195 in_memory=True, 196 ), 197 # Allow the user to set the path to lookup MCP server commands, like npx. 198 "custom_mcp_path": ConfigProperty( 199 str, 200 env_var="CUSTOM_MCP_PATH", 201 ), 202 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 203 MCP_SECRETS_KEY: ConfigProperty( 204 dict[str, str], 205 sensitive=True, 206 ), 207 "git_sync_projects": ConfigProperty( 208 dict, 209 default_lambda=lambda: {}, 210 sensitive_keys=["pat_token", "oauth_token"], 211 ), 212 # has the user indicated it's for personal or work use? 213 "user_type": ConfigProperty( 214 str, # "personal" or "work" 215 ), 216 # if the user has provided their work contact 217 "work_use_contact": ConfigProperty( 218 str, 219 ), 220 # if the user has provided their personal contact 221 "personal_use_contact": ConfigProperty( 222 str, 223 ), 224 } 225 self._lock = threading.Lock() 226 self._in_memory_settings: Dict[str, Any] = {} 227 self._settings = self.load_settings() 228 229 @classmethod 230 def shared(cls): 231 if cls._shared_instance is None: 232 cls._shared_instance = cls() 233 return cls._shared_instance 234 235 # Get a value, mockable for testing 236 def get_value(self, name: str) -> Any: 237 try: 238 return self.__getattr__(name) 239 except AttributeError: 240 return None 241 242 def kiln_local_api_base_url(self) -> str: 243 return f"http://{self.kiln_local_api_host}:{self.kiln_local_api_port}" 244 245 def __getattr__(self, name: str) -> Any: 246 if name == "_properties": 247 return super().__getattribute__("_properties") 248 if name not in self._properties: 249 return super().__getattribute__(name) 250 251 property_config = self._properties[name] 252 253 if property_config.in_memory: 254 if name in self._in_memory_settings: 255 value = self._in_memory_settings[name] 256 return value if value is None else property_config.type(value) 257 else: 258 if name in self._settings: 259 value = self._settings[name] 260 return value if value is None else property_config.type(value) 261 262 # Check environment variable 263 if property_config.env_var and property_config.env_var in os.environ: 264 value = os.environ[property_config.env_var] 265 return property_config.type(value) 266 267 # Use default value or default_lambda 268 if property_config.default_lambda: 269 value = property_config.default_lambda() 270 else: 271 value = property_config.default 272 273 return None if value is None else property_config.type(value) 274 275 def __setattr__(self, name, value): 276 if name in ("_properties", "_settings", "_lock", "_in_memory_settings"): 277 super().__setattr__(name, value) 278 elif name in self._properties: 279 if self._properties[name].in_memory: 280 with self._lock: 281 self._in_memory_settings[name] = value 282 else: 283 self.update_settings({name: value}) 284 else: 285 raise AttributeError(f"Config has no attribute '{name}'") 286 287 @classmethod 288 def settings_dir(cls, create=True) -> str: 289 settings_dir = os.path.join(Path.home(), ".kiln_ai") 290 if create: 291 os.makedirs(settings_dir, exist_ok=True) 292 return settings_dir 293 294 @classmethod 295 def settings_path(cls, create=True) -> str: 296 settings_dir = cls.settings_dir(create) 297 return os.path.join(settings_dir, "settings.yaml") 298 299 @classmethod 300 def load_settings(cls): 301 if not os.path.isfile(cls.settings_path(create=False)): 302 return {} 303 with open(cls.settings_path(), "r") as f: 304 settings = yaml.safe_load(f.read()) or {} 305 return settings 306 307 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 308 with self._lock: 309 filtered_disk = { 310 k: v 311 for k, v in self._settings.items() 312 if k not in self._properties or not self._properties[k].in_memory 313 } 314 combined = {**filtered_disk, **self._in_memory_settings} 315 316 if not hide_sensitive: 317 return combined 318 319 settings = { 320 k: "[hidden]" 321 if k in self._properties and self._properties[k].sensitive 322 else copy.deepcopy(v) 323 for k, v in combined.items() 324 } 325 # Hide sensitive keys in nested structures (lists of dicts, or dicts of dicts) 326 for key, value in settings.items(): 327 if key in self._properties and self._properties[key].sensitive_keys: 328 sensitive_keys = self._properties[key].sensitive_keys or [] 329 for sensitive_key in sensitive_keys: 330 if isinstance(value, list): 331 for item in value: 332 if isinstance(item, dict) and sensitive_key in item: 333 item[sensitive_key] = "[hidden]" 334 elif isinstance(value, dict): 335 if sensitive_key in value: 336 value[sensitive_key] = "[hidden]" 337 for item in value.values(): 338 if isinstance(item, dict) and sensitive_key in item: 339 item[sensitive_key] = "[hidden]" 340 341 return settings 342 343 def save_setting(self, name: str, value: Any): 344 self.update_settings({name: value}) 345 346 def update_settings(self, new_settings: Dict[str, Any]): 347 with self._lock: 348 in_memory_updates = { 349 k: v 350 for k, v in new_settings.items() 351 if k in self._properties and self._properties[k].in_memory 352 } 353 disk_updates = { 354 k: v 355 for k, v in new_settings.items() 356 if k not in self._properties or not self._properties[k].in_memory 357 } 358 359 if in_memory_updates: 360 self._in_memory_settings.update(in_memory_updates) 361 362 if disk_updates: 363 # Fresh load to avoid clobbering changes from other instances 364 current_settings = self.load_settings() 365 current_settings.update(disk_updates) 366 # remove None values 367 current_settings = { 368 k: v for k, v in current_settings.items() if v is not None 369 } 370 with open(self.settings_path(), "w") as f: 371 yaml.dump(current_settings, f) 372 self._settings = current_settings 373 374 375def _get_user_id(): 376 try: 377 return getpass.getuser() or "unknown_user" 378 except Exception: 379 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 "featherless_ai_api_key": ConfigProperty( 172 str, 173 env_var="FEATHERLESS_AI_API_KEY", 174 sensitive=True, 175 ), 176 "kiln_copilot_api_key": ConfigProperty( 177 str, 178 env_var="KILN_COPILOT_API_KEY", 179 sensitive=True, 180 ), 181 "enable_demo_tools": ConfigProperty( 182 bool, 183 env_var="ENABLE_DEMO_TOOLS", 184 default=False, 185 ), 186 "kiln_local_api_host": ConfigProperty( 187 str, 188 env_var="KILN_LOCAL_API_HOST", 189 default="127.0.0.1", 190 in_memory=True, 191 ), 192 "kiln_local_api_port": ConfigProperty( 193 int, 194 env_var="KILN_LOCAL_API_PORT", 195 default=8757, 196 in_memory=True, 197 ), 198 # Allow the user to set the path to lookup MCP server commands, like npx. 199 "custom_mcp_path": ConfigProperty( 200 str, 201 env_var="CUSTOM_MCP_PATH", 202 ), 203 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 204 MCP_SECRETS_KEY: ConfigProperty( 205 dict[str, str], 206 sensitive=True, 207 ), 208 "git_sync_projects": ConfigProperty( 209 dict, 210 default_lambda=lambda: {}, 211 sensitive_keys=["pat_token", "oauth_token"], 212 ), 213 # has the user indicated it's for personal or work use? 214 "user_type": ConfigProperty( 215 str, # "personal" or "work" 216 ), 217 # if the user has provided their work contact 218 "work_use_contact": ConfigProperty( 219 str, 220 ), 221 # if the user has provided their personal contact 222 "personal_use_contact": ConfigProperty( 223 str, 224 ), 225 } 226 self._lock = threading.Lock() 227 self._in_memory_settings: Dict[str, Any] = {} 228 self._settings = self.load_settings() 229 230 @classmethod 231 def shared(cls): 232 if cls._shared_instance is None: 233 cls._shared_instance = cls() 234 return cls._shared_instance 235 236 # Get a value, mockable for testing 237 def get_value(self, name: str) -> Any: 238 try: 239 return self.__getattr__(name) 240 except AttributeError: 241 return None 242 243 def kiln_local_api_base_url(self) -> str: 244 return f"http://{self.kiln_local_api_host}:{self.kiln_local_api_port}" 245 246 def __getattr__(self, name: str) -> Any: 247 if name == "_properties": 248 return super().__getattribute__("_properties") 249 if name not in self._properties: 250 return super().__getattribute__(name) 251 252 property_config = self._properties[name] 253 254 if property_config.in_memory: 255 if name in self._in_memory_settings: 256 value = self._in_memory_settings[name] 257 return value if value is None else property_config.type(value) 258 else: 259 if name in self._settings: 260 value = self._settings[name] 261 return value if value is None else property_config.type(value) 262 263 # Check environment variable 264 if property_config.env_var and property_config.env_var in os.environ: 265 value = os.environ[property_config.env_var] 266 return property_config.type(value) 267 268 # Use default value or default_lambda 269 if property_config.default_lambda: 270 value = property_config.default_lambda() 271 else: 272 value = property_config.default 273 274 return None if value is None else property_config.type(value) 275 276 def __setattr__(self, name, value): 277 if name in ("_properties", "_settings", "_lock", "_in_memory_settings"): 278 super().__setattr__(name, value) 279 elif name in self._properties: 280 if self._properties[name].in_memory: 281 with self._lock: 282 self._in_memory_settings[name] = value 283 else: 284 self.update_settings({name: value}) 285 else: 286 raise AttributeError(f"Config has no attribute '{name}'") 287 288 @classmethod 289 def settings_dir(cls, create=True) -> str: 290 settings_dir = os.path.join(Path.home(), ".kiln_ai") 291 if create: 292 os.makedirs(settings_dir, exist_ok=True) 293 return settings_dir 294 295 @classmethod 296 def settings_path(cls, create=True) -> str: 297 settings_dir = cls.settings_dir(create) 298 return os.path.join(settings_dir, "settings.yaml") 299 300 @classmethod 301 def load_settings(cls): 302 if not os.path.isfile(cls.settings_path(create=False)): 303 return {} 304 with open(cls.settings_path(), "r") as f: 305 settings = yaml.safe_load(f.read()) or {} 306 return settings 307 308 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 309 with self._lock: 310 filtered_disk = { 311 k: v 312 for k, v in self._settings.items() 313 if k not in self._properties or not self._properties[k].in_memory 314 } 315 combined = {**filtered_disk, **self._in_memory_settings} 316 317 if not hide_sensitive: 318 return combined 319 320 settings = { 321 k: "[hidden]" 322 if k in self._properties and self._properties[k].sensitive 323 else copy.deepcopy(v) 324 for k, v in combined.items() 325 } 326 # Hide sensitive keys in nested structures (lists of dicts, or dicts of dicts) 327 for key, value in settings.items(): 328 if key in self._properties and self._properties[key].sensitive_keys: 329 sensitive_keys = self._properties[key].sensitive_keys or [] 330 for sensitive_key in sensitive_keys: 331 if isinstance(value, list): 332 for item in value: 333 if isinstance(item, dict) and sensitive_key in item: 334 item[sensitive_key] = "[hidden]" 335 elif isinstance(value, dict): 336 if sensitive_key in value: 337 value[sensitive_key] = "[hidden]" 338 for item in value.values(): 339 if isinstance(item, dict) and sensitive_key in item: 340 item[sensitive_key] = "[hidden]" 341 342 return settings 343 344 def save_setting(self, name: str, value: Any): 345 self.update_settings({name: value}) 346 347 def update_settings(self, new_settings: Dict[str, Any]): 348 with self._lock: 349 in_memory_updates = { 350 k: v 351 for k, v in new_settings.items() 352 if k in self._properties and self._properties[k].in_memory 353 } 354 disk_updates = { 355 k: v 356 for k, v in new_settings.items() 357 if k not in self._properties or not self._properties[k].in_memory 358 } 359 360 if in_memory_updates: 361 self._in_memory_settings.update(in_memory_updates) 362 363 if disk_updates: 364 # Fresh load to avoid clobbering changes from other instances 365 current_settings = self.load_settings() 366 current_settings.update(disk_updates) 367 # remove None values 368 current_settings = { 369 k: v for k, v in current_settings.items() if v is not None 370 } 371 with open(self.settings_path(), "w") as f: 372 yaml.dump(current_settings, f) 373 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 "featherless_ai_api_key": ConfigProperty( 172 str, 173 env_var="FEATHERLESS_AI_API_KEY", 174 sensitive=True, 175 ), 176 "kiln_copilot_api_key": ConfigProperty( 177 str, 178 env_var="KILN_COPILOT_API_KEY", 179 sensitive=True, 180 ), 181 "enable_demo_tools": ConfigProperty( 182 bool, 183 env_var="ENABLE_DEMO_TOOLS", 184 default=False, 185 ), 186 "kiln_local_api_host": ConfigProperty( 187 str, 188 env_var="KILN_LOCAL_API_HOST", 189 default="127.0.0.1", 190 in_memory=True, 191 ), 192 "kiln_local_api_port": ConfigProperty( 193 int, 194 env_var="KILN_LOCAL_API_PORT", 195 default=8757, 196 in_memory=True, 197 ), 198 # Allow the user to set the path to lookup MCP server commands, like npx. 199 "custom_mcp_path": ConfigProperty( 200 str, 201 env_var="CUSTOM_MCP_PATH", 202 ), 203 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 204 MCP_SECRETS_KEY: ConfigProperty( 205 dict[str, str], 206 sensitive=True, 207 ), 208 "git_sync_projects": ConfigProperty( 209 dict, 210 default_lambda=lambda: {}, 211 sensitive_keys=["pat_token", "oauth_token"], 212 ), 213 # has the user indicated it's for personal or work use? 214 "user_type": ConfigProperty( 215 str, # "personal" or "work" 216 ), 217 # if the user has provided their work contact 218 "work_use_contact": ConfigProperty( 219 str, 220 ), 221 # if the user has provided their personal contact 222 "personal_use_contact": ConfigProperty( 223 str, 224 ), 225 } 226 self._lock = threading.Lock() 227 self._in_memory_settings: Dict[str, Any] = {} 228 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
308 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 309 with self._lock: 310 filtered_disk = { 311 k: v 312 for k, v in self._settings.items() 313 if k not in self._properties or not self._properties[k].in_memory 314 } 315 combined = {**filtered_disk, **self._in_memory_settings} 316 317 if not hide_sensitive: 318 return combined 319 320 settings = { 321 k: "[hidden]" 322 if k in self._properties and self._properties[k].sensitive 323 else copy.deepcopy(v) 324 for k, v in combined.items() 325 } 326 # Hide sensitive keys in nested structures (lists of dicts, or dicts of dicts) 327 for key, value in settings.items(): 328 if key in self._properties and self._properties[key].sensitive_keys: 329 sensitive_keys = self._properties[key].sensitive_keys or [] 330 for sensitive_key in sensitive_keys: 331 if isinstance(value, list): 332 for item in value: 333 if isinstance(item, dict) and sensitive_key in item: 334 item[sensitive_key] = "[hidden]" 335 elif isinstance(value, dict): 336 if sensitive_key in value: 337 value[sensitive_key] = "[hidden]" 338 for item in value.values(): 339 if isinstance(item, dict) and sensitive_key in item: 340 item[sensitive_key] = "[hidden]" 341 342 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
347 def update_settings(self, new_settings: Dict[str, Any]): 348 with self._lock: 349 in_memory_updates = { 350 k: v 351 for k, v in new_settings.items() 352 if k in self._properties and self._properties[k].in_memory 353 } 354 disk_updates = { 355 k: v 356 for k, v in new_settings.items() 357 if k not in self._properties or not self._properties[k].in_memory 358 } 359 360 if in_memory_updates: 361 self._in_memory_settings.update(in_memory_updates) 362 363 if disk_updates: 364 # Fresh load to avoid clobbering changes from other instances 365 current_settings = self.load_settings() 366 current_settings.update(disk_updates) 367 # remove None values 368 current_settings = { 369 k: v for k, v in current_settings.items() if v is not None 370 } 371 with open(self.settings_path(), "w") as f: 372 yaml.dump(current_settings, f) 373 self._settings = current_settings