Skip to content

Commit 20f9c0e

Browse files
committed
Refactor app config into modular components
1 parent c03159e commit 20f9c0e

File tree

3 files changed

+1900
-2007
lines changed

3 files changed

+1900
-2007
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
from typing import Any, cast
5+
6+
from pydantic import ConfigDict, Field, field_validator, model_validator
7+
8+
from src.core.domain.configuration.app_identity_config import AppIdentityConfig
9+
from src.core.interfaces.model_bases import DomainModel
10+
from src.core.services.backend_registry import backend_registry
11+
12+
13+
class LogLevel(str, Enum):
14+
"""Log levels for configuration."""
15+
16+
DEBUG = "DEBUG"
17+
INFO = "INFO"
18+
WARNING = "WARNING"
19+
ERROR = "ERROR"
20+
CRITICAL = "CRITICAL"
21+
22+
23+
class BackendConfig(DomainModel):
24+
"""Configuration for a backend service."""
25+
26+
model_config = ConfigDict(frozen=True)
27+
28+
api_key: list[str] = Field(default_factory=list)
29+
api_url: str | None = None
30+
models: list[str] = Field(default_factory=list)
31+
timeout: int = 120
32+
identity: AppIdentityConfig | None = None
33+
extra: dict[str, Any] = Field(default_factory=dict)
34+
35+
@field_validator("api_key", mode="before")
36+
@classmethod
37+
def validate_api_key(cls, value: Any) -> list[str]:
38+
if isinstance(value, str):
39+
return [value]
40+
return value if isinstance(value, list) else []
41+
42+
@field_validator("api_url")
43+
@classmethod
44+
def validate_api_url(cls, value: str | None) -> str | None:
45+
if value is not None and not value.startswith(("http://", "https://")):
46+
raise ValueError("API URL must start with http:// or https://")
47+
return value
48+
49+
50+
class BruteForceProtectionConfig(DomainModel):
51+
"""Configuration for brute-force protection on API authentication."""
52+
53+
model_config = ConfigDict(frozen=True)
54+
55+
enabled: bool = True
56+
max_failed_attempts: int = 5
57+
ttl_seconds: int = 900
58+
initial_block_seconds: int = 30
59+
block_multiplier: float = 2.0
60+
max_block_seconds: int = 3600
61+
62+
63+
class AuthConfig(DomainModel):
64+
"""Authentication configuration."""
65+
66+
model_config = ConfigDict(frozen=True)
67+
68+
disable_auth: bool = False
69+
api_keys: list[str] = Field(default_factory=list)
70+
auth_token: str | None = None
71+
redact_api_keys_in_prompts: bool = True
72+
trusted_ips: list[str] = Field(default_factory=list)
73+
brute_force_protection: BruteForceProtectionConfig = Field(
74+
default_factory=BruteForceProtectionConfig
75+
)
76+
77+
78+
class LoggingConfig(DomainModel):
79+
"""Logging configuration."""
80+
81+
model_config = ConfigDict(frozen=True)
82+
83+
level: LogLevel = LogLevel.INFO
84+
request_logging: bool = False
85+
response_logging: bool = False
86+
log_file: str | None = None
87+
capture_file: str | None = None
88+
capture_max_bytes: int | None = None
89+
capture_truncate_bytes: int | None = None
90+
capture_max_files: int | None = None
91+
capture_rotate_interval_seconds: int = 86400
92+
capture_total_max_bytes: int = 104857600
93+
capture_buffer_size: int = 65536
94+
capture_flush_interval: float = 1.0
95+
capture_max_entries_per_flush: int = 100
96+
97+
98+
class ToolCallReactorConfig(DomainModel):
99+
"""Configuration for the Tool Call Reactor system."""
100+
101+
model_config = ConfigDict(frozen=True)
102+
103+
enabled: bool = True
104+
apply_diff_steering_enabled: bool = True
105+
apply_diff_steering_rate_limit_seconds: int = 60
106+
apply_diff_steering_message: str | None = None
107+
pytest_full_suite_steering_enabled: bool = False
108+
pytest_full_suite_steering_message: str | None = None
109+
pytest_context_saving_enabled: bool = False
110+
fix_think_tags_enabled: bool = False
111+
steering_rules: list[dict[str, Any]] = Field(default_factory=list)
112+
access_policies: list[dict[str, Any]] = Field(default_factory=list)
113+
114+
115+
class PlanningPhaseConfig(DomainModel):
116+
"""Configuration for planning phase model routing."""
117+
118+
model_config = ConfigDict(frozen=True)
119+
120+
enabled: bool = False
121+
strong_model: str | None = None
122+
max_turns: int = 10
123+
max_file_writes: int = 1
124+
overrides: dict[str, Any] | None = None
125+
126+
127+
class SessionContinuityConfig(DomainModel):
128+
"""Configuration for intelligent session continuity detection."""
129+
130+
model_config = ConfigDict(frozen=True)
131+
132+
enabled: bool = True
133+
fuzzy_matching: bool = True
134+
max_session_age_seconds: int = 604800
135+
fingerprint_message_count: int = 5
136+
client_key_includes_ip: bool = True
137+
138+
139+
class SessionConfig(DomainModel):
140+
"""Session management configuration."""
141+
142+
model_config = ConfigDict(frozen=True)
143+
144+
cleanup_enabled: bool = True
145+
cleanup_interval: int = 3600
146+
max_age: int = 86400
147+
default_interactive_mode: bool = True
148+
force_set_project: bool = False
149+
disable_interactive_commands: bool = False
150+
project_dir_resolution_model: str | None = None
151+
project_dir_resolution_mode: str = "hybrid"
152+
tool_call_repair_enabled: bool = True
153+
tool_call_repair_buffer_cap_bytes: int = 64 * 1024
154+
json_repair_enabled: bool = True
155+
json_repair_buffer_cap_bytes: int = 64 * 1024
156+
json_repair_strict_mode: bool = False
157+
json_repair_schema: dict[str, Any] | None = None
158+
tool_call_reactor: ToolCallReactorConfig = Field(default_factory=ToolCallReactorConfig)
159+
dangerous_command_prevention_enabled: bool = True
160+
dangerous_command_steering_message: str | None = None
161+
pytest_compression_enabled: bool = True
162+
pytest_compression_min_lines: int = 30
163+
pytest_full_suite_steering_enabled: bool | None = None
164+
pytest_full_suite_steering_message: str | None = None
165+
fix_think_tags_enabled: bool = False
166+
fix_think_tags_streaming_buffer_size: int = 4096
167+
planning_phase: PlanningPhaseConfig = Field(default_factory=PlanningPhaseConfig)
168+
max_per_session_backends: int = 32
169+
session_continuity: SessionContinuityConfig = Field(
170+
default_factory=SessionContinuityConfig
171+
)
172+
tool_access_global_overrides: dict[str, Any] | None = None
173+
force_reprocess_tool_calls: bool = False
174+
log_skipped_tool_calls: bool = False
175+
176+
@model_validator(mode="before")
177+
@classmethod
178+
def _sync_pytest_full_suite_settings(cls, values: dict[str, Any]) -> dict[str, Any]:
179+
reactor_config = values.get("tool_call_reactor")
180+
if isinstance(reactor_config, ToolCallReactorConfig):
181+
reactor_config_dict = reactor_config.model_dump()
182+
elif isinstance(reactor_config, dict):
183+
reactor_config_dict = dict(reactor_config)
184+
else:
185+
reactor_config_dict = {}
186+
187+
enabled = values.get("pytest_full_suite_steering_enabled")
188+
message = values.get("pytest_full_suite_steering_message")
189+
190+
if enabled is not None:
191+
reactor_config_dict["pytest_full_suite_steering_enabled"] = enabled
192+
else:
193+
values["pytest_full_suite_steering_enabled"] = reactor_config_dict.get(
194+
"pytest_full_suite_steering_enabled", False
195+
)
196+
197+
if message is not None:
198+
reactor_config_dict["pytest_full_suite_steering_message"] = message
199+
else:
200+
values["pytest_full_suite_steering_message"] = reactor_config_dict.get(
201+
"pytest_full_suite_steering_message"
202+
)
203+
204+
values["tool_call_reactor"] = reactor_config_dict
205+
return values
206+
207+
208+
class EmptyResponseConfig(DomainModel):
209+
"""Configuration for empty response handling."""
210+
211+
model_config = ConfigDict(frozen=True)
212+
213+
enabled: bool = True
214+
max_retries: int = 1
215+
216+
217+
class ModelAliasRule(DomainModel):
218+
"""Rule for remapping model names."""
219+
220+
model_config = ConfigDict(frozen=True)
221+
222+
pattern: str
223+
replacement: str
224+
225+
226+
class RewritingConfig(DomainModel):
227+
"""Configuration for content rewriting."""
228+
229+
model_config = ConfigDict(frozen=True)
230+
231+
enabled: bool = False
232+
config_path: str = "config/replacements"
233+
234+
235+
class EditPrecisionConfig(DomainModel):
236+
"""Configuration for automated edit-precision tuning."""
237+
238+
model_config = ConfigDict(frozen=True)
239+
240+
enabled: bool = True
241+
temperature: float = 0.1
242+
min_top_p: float | None = 0.3
243+
override_top_p: bool = False
244+
override_top_k: bool = False
245+
target_top_k: int | None = None
246+
exclude_agents_regex: str | None = None
247+
248+
249+
class BackendSettings(DomainModel):
250+
"""Settings for all backends."""
251+
252+
model_config = ConfigDict(frozen=False, extra="allow")
253+
254+
default_backend: str = "openai"
255+
static_route: str | None = None
256+
disable_gemini_oauth_fallback: bool = False
257+
disable_hybrid_backend: bool = False
258+
hybrid_backend_repeat_messages: bool = False
259+
reasoning_injection_probability: float = Field(
260+
default=1.0,
261+
ge=0.0,
262+
le=1.0,
263+
description="Probability of using the reasoning model for a request in the hybrid backend.",
264+
)
265+
266+
def __init__(self, **data: Any) -> None:
267+
known_fields = set(self.model_fields.keys())
268+
init_data = {key: value for key, value in data.items() if key in known_fields}
269+
backend_data = {key: value for key, value in data.items() if key not in known_fields}
270+
super().__init__(**init_data)
271+
272+
for backend_name, config_data in backend_data.items():
273+
if isinstance(config_data, dict):
274+
self.__dict__[backend_name] = BackendConfig(**config_data)
275+
elif isinstance(config_data, BackendConfig):
276+
self.__dict__[backend_name] = config_data
277+
278+
for backend_name in backend_registry.get_registered_backends():
279+
if backend_name not in self.__dict__:
280+
self.__dict__[backend_name] = BackendConfig()
281+
282+
self._initialization_complete = True
283+
284+
def __getitem__(self, key: str) -> BackendConfig:
285+
if key in self.__dict__:
286+
return cast(BackendConfig, self.__dict__[key])
287+
raise KeyError(f"Backend '{key}' not found")
288+
289+
def __setitem__(self, key: str, value: BackendConfig) -> None:
290+
self.__dict__[key] = value
291+
292+
def __setattr__(self, name: str, value: Any) -> None:
293+
if name in {"default_backend"} or name.startswith("_") or name in self.model_fields:
294+
super().__setattr__(name, value)
295+
return
296+
if isinstance(value, BackendConfig):
297+
config = value
298+
elif isinstance(value, dict):
299+
config = BackendConfig(**value)
300+
else:
301+
config = BackendConfig()
302+
self.__dict__[name] = config
303+
304+
def get(self, key: str, default: Any = None) -> Any:
305+
return cast(BackendConfig | None, self.__dict__.get(key, default))
306+
307+
@property
308+
def functional_backends(self) -> set[str]:
309+
functional: set[str] = set()
310+
registered = backend_registry.get_registered_backends()
311+
for backend_name in registered:
312+
if backend_name in self.__dict__:
313+
config = self.__dict__[backend_name]
314+
if isinstance(config, BackendConfig) and config.api_key:
315+
functional.add(backend_name)
316+
317+
oauth_like: set[str] = set()
318+
for name in registered:
319+
if name.endswith("-oauth") or name.startswith("gemini-oauth"):
320+
oauth_like.add(name)
321+
if name == "gemini-cli-cloud-project":
322+
oauth_like.add(name)
323+
324+
functional.update(oauth_like.intersection(set(registered)))
325+
326+
for name, cfg in getattr(self, "__dict__", {}).items():
327+
if (
328+
name == "default_backend"
329+
or name.startswith("_")
330+
or not isinstance(cfg, BackendConfig)
331+
):
332+
continue
333+
if cfg.api_key:
334+
functional.add(name)
335+
return functional
336+
337+
def __getattr__(self, name: str) -> Any:
338+
if name == "default_backend":
339+
if "default_backend" in self.__dict__:
340+
return self.__dict__["default_backend"]
341+
return "openai"
342+
343+
if name in self.__dict__:
344+
return cast(BackendConfig, self.__dict__[name])
345+
346+
if name.startswith(("_", "__")):
347+
raise AttributeError(
348+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
349+
)
350+
351+
if not hasattr(self, "_initialization_complete"):
352+
raise AttributeError(
353+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
354+
)
355+
356+
config = BackendConfig()
357+
self.__dict__[name] = config
358+
return config
359+
360+
def set(self, key: str, value: Any) -> None:
361+
setattr(self, key, value)
362+
363+
def get_gcp_project_id(self) -> str | None:
364+
return self.gcp_project_id
365+
366+
367+
__all__ = [
368+
"AuthConfig",
369+
"BackendConfig",
370+
"BackendSettings",
371+
"BruteForceProtectionConfig",
372+
"EditPrecisionConfig",
373+
"EmptyResponseConfig",
374+
"LogLevel",
375+
"LoggingConfig",
376+
"ModelAliasRule",
377+
"PlanningPhaseConfig",
378+
"RewritingConfig",
379+
"SessionConfig",
380+
"SessionContinuityConfig",
381+
"ToolCallReactorConfig",
382+
]

0 commit comments

Comments
 (0)