ccproxy-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,534 @@
1
+ """Settings configuration for Claude Proxy API Server."""
2
+
3
+ import contextlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ import tomllib
8
+ from pathlib import Path
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
+ from pydantic_settings import BaseSettings, SettingsConfigDict
13
+
14
+ from ccproxy import __version__
15
+ from ccproxy.config.discovery import find_toml_config_file, get_claude_cli_config_dir
16
+ from ccproxy.core.async_utils import format_version, get_package_dir, patched_typing
17
+
18
+ from .auth import AuthSettings
19
+ from .claude import ClaudeSettings
20
+ from .cors import CORSSettings
21
+ from .docker_settings import DockerSettings
22
+ from .observability import ObservabilitySettings
23
+ from .pricing import PricingSettings
24
+ from .reverse_proxy import ReverseProxySettings
25
+ from .scheduler import SchedulerSettings
26
+ from .security import SecuritySettings
27
+ from .server import ServerSettings
28
+
29
+
30
+ __all__ = [
31
+ "Settings",
32
+ "ConfigurationError",
33
+ "ConfigurationManager",
34
+ "config_manager",
35
+ "get_settings",
36
+ ]
37
+
38
+
39
+ class ConfigurationError(Exception):
40
+ """Raised when configuration loading or validation fails."""
41
+
42
+ pass
43
+
44
+
45
+ # PoolSettings class removed - connection pooling functionality has been removed
46
+
47
+
48
+ class Settings(BaseSettings):
49
+ """
50
+ Configuration settings for the Claude Proxy API Server.
51
+
52
+ Settings are loaded from environment variables, .env files, and TOML configuration files.
53
+ Environment variables take precedence over .env file values.
54
+ TOML configuration files are loaded in the following order:
55
+ 1. .ccproxy.toml in current directory
56
+ 2. ccproxy.toml in git repository root
57
+ 3. config.toml in XDG_CONFIG_HOME/ccproxy/
58
+ """
59
+
60
+ model_config = SettingsConfigDict(
61
+ env_file=".env",
62
+ env_file_encoding="utf-8",
63
+ case_sensitive=False,
64
+ extra="ignore",
65
+ env_nested_delimiter="__",
66
+ )
67
+
68
+ # Core application settings
69
+ server: ServerSettings = Field(
70
+ default_factory=ServerSettings,
71
+ description="Server configuration settings",
72
+ )
73
+
74
+ security: SecuritySettings = Field(
75
+ default_factory=SecuritySettings,
76
+ description="Security configuration settings",
77
+ )
78
+
79
+ cors: CORSSettings = Field(
80
+ default_factory=CORSSettings,
81
+ description="CORS configuration settings",
82
+ )
83
+
84
+ # Claude-specific settings
85
+ claude: ClaudeSettings = Field(
86
+ default_factory=ClaudeSettings,
87
+ description="Claude-specific configuration settings",
88
+ )
89
+
90
+ # Proxy and authentication
91
+ reverse_proxy: ReverseProxySettings = Field(
92
+ default_factory=ReverseProxySettings,
93
+ description="Reverse proxy configuration settings",
94
+ )
95
+
96
+ auth: AuthSettings = Field(
97
+ default_factory=AuthSettings,
98
+ description="Authentication and credentials configuration",
99
+ )
100
+
101
+ # Container settings
102
+ docker: DockerSettings = Field(
103
+ default_factory=DockerSettings,
104
+ description="Docker configuration for running Claude commands in containers",
105
+ )
106
+
107
+ # Observability settings
108
+ observability: ObservabilitySettings = Field(
109
+ default_factory=ObservabilitySettings,
110
+ description="Observability configuration settings",
111
+ )
112
+
113
+ # Scheduler settings
114
+ scheduler: SchedulerSettings = Field(
115
+ default_factory=SchedulerSettings,
116
+ description="Task scheduler configuration settings",
117
+ )
118
+
119
+ # Pricing settings
120
+ pricing: PricingSettings = Field(
121
+ default_factory=PricingSettings,
122
+ description="Pricing and cost calculation configuration settings",
123
+ )
124
+
125
+ @field_validator("server", mode="before")
126
+ @classmethod
127
+ def validate_server(cls, v: Any) -> Any:
128
+ """Validate and convert server settings."""
129
+ if v is None:
130
+ return ServerSettings()
131
+ if isinstance(v, ServerSettings):
132
+ return v
133
+ if isinstance(v, dict):
134
+ return ServerSettings(**v)
135
+ return v
136
+
137
+ @field_validator("security", mode="before")
138
+ @classmethod
139
+ def validate_security(cls, v: Any) -> Any:
140
+ """Validate and convert security settings."""
141
+ if v is None:
142
+ return SecuritySettings()
143
+ if isinstance(v, SecuritySettings):
144
+ return v
145
+ if isinstance(v, dict):
146
+ return SecuritySettings(**v)
147
+ return v
148
+
149
+ @field_validator("cors", mode="before")
150
+ @classmethod
151
+ def validate_cors(cls, v: Any) -> Any:
152
+ """Validate and convert CORS settings."""
153
+ if v is None:
154
+ return CORSSettings()
155
+ if isinstance(v, CORSSettings):
156
+ return v
157
+ if isinstance(v, dict):
158
+ return CORSSettings(**v)
159
+ return v
160
+
161
+ @field_validator("claude", mode="before")
162
+ @classmethod
163
+ def validate_claude(cls, v: Any) -> Any:
164
+ """Validate and convert Claude settings."""
165
+ if v is None:
166
+ return ClaudeSettings()
167
+ if isinstance(v, ClaudeSettings):
168
+ return v
169
+ if isinstance(v, dict):
170
+ return ClaudeSettings(**v)
171
+ return v
172
+
173
+ @field_validator("reverse_proxy", mode="before")
174
+ @classmethod
175
+ def validate_reverse_proxy(cls, v: Any) -> Any:
176
+ """Validate and convert reverse proxy settings."""
177
+ if v is None:
178
+ return ReverseProxySettings()
179
+ if isinstance(v, ReverseProxySettings):
180
+ return v
181
+ if isinstance(v, dict):
182
+ return ReverseProxySettings(**v)
183
+ return v
184
+
185
+ @field_validator("auth", mode="before")
186
+ @classmethod
187
+ def validate_auth(cls, v: Any) -> Any:
188
+ """Validate and convert auth settings."""
189
+ if v is None:
190
+ return AuthSettings()
191
+ if isinstance(v, AuthSettings):
192
+ return v
193
+ if isinstance(v, dict):
194
+ return AuthSettings(**v)
195
+ return v
196
+
197
+ @field_validator("docker", mode="before")
198
+ @classmethod
199
+ def validate_docker_settings(cls, v: Any) -> Any:
200
+ """Validate and convert Docker settings."""
201
+ if v is None:
202
+ return DockerSettings()
203
+
204
+ # If it's already a DockerSettings instance, return as-is
205
+ if isinstance(v, DockerSettings):
206
+ return v
207
+
208
+ # If it's a dict, create DockerSettings from it
209
+ if isinstance(v, dict):
210
+ return DockerSettings(**v)
211
+
212
+ # Try to convert to dict if possible
213
+ if hasattr(v, "model_dump"):
214
+ return DockerSettings(**v.model_dump())
215
+ elif hasattr(v, "__dict__"):
216
+ return DockerSettings(**v.__dict__)
217
+
218
+ return v
219
+
220
+ @field_validator("observability", mode="before")
221
+ @classmethod
222
+ def validate_observability(cls, v: Any) -> Any:
223
+ """Validate and convert observability settings."""
224
+ if v is None:
225
+ return ObservabilitySettings()
226
+ if isinstance(v, ObservabilitySettings):
227
+ return v
228
+ if isinstance(v, dict):
229
+ return ObservabilitySettings(**v)
230
+ return v
231
+
232
+ @field_validator("scheduler", mode="before")
233
+ @classmethod
234
+ def validate_scheduler(cls, v: Any) -> Any:
235
+ """Validate and convert scheduler settings."""
236
+ if v is None:
237
+ return SchedulerSettings()
238
+ if isinstance(v, SchedulerSettings):
239
+ return v
240
+ if isinstance(v, dict):
241
+ return SchedulerSettings(**v)
242
+ return v
243
+
244
+ @field_validator("pricing", mode="before")
245
+ @classmethod
246
+ def validate_pricing(cls, v: Any) -> Any:
247
+ """Validate and convert pricing settings."""
248
+ if v is None:
249
+ return PricingSettings()
250
+ if isinstance(v, PricingSettings):
251
+ return v
252
+ if isinstance(v, dict):
253
+ return PricingSettings(**v)
254
+ return v
255
+
256
+ # validate_pool_settings method removed - connection pooling functionality has been removed
257
+
258
+ @property
259
+ def server_url(self) -> str:
260
+ """Get the complete server URL."""
261
+ return f"http://{self.server.host}:{self.server.port}"
262
+
263
+ @property
264
+ def is_development(self) -> bool:
265
+ """Check if running in development mode."""
266
+ return self.server.reload or self.server.log_level == "DEBUG"
267
+
268
+ @model_validator(mode="after")
269
+ def setup_claude_cli_path(self) -> "Settings":
270
+ """Set up Claude CLI path in environment if provided or found."""
271
+ # If not explicitly set, try to find it
272
+ if not self.claude.cli_path:
273
+ found_path, found_in_path = self.claude.find_claude_cli()
274
+ if found_path:
275
+ self.claude.cli_path = found_path
276
+ # Only add to PATH if it wasn't found via which()
277
+ if not found_in_path:
278
+ cli_dir = str(Path(self.claude.cli_path).parent)
279
+ current_path = os.environ.get("PATH", "")
280
+ if cli_dir not in current_path:
281
+ os.environ["PATH"] = f"{cli_dir}:{current_path}"
282
+ elif self.claude.cli_path:
283
+ # If explicitly set, always add to PATH
284
+ cli_dir = str(Path(self.claude.cli_path).parent)
285
+ current_path = os.environ.get("PATH", "")
286
+ if cli_dir not in current_path:
287
+ os.environ["PATH"] = f"{cli_dir}:{current_path}"
288
+ return self
289
+
290
+ def model_dump_safe(self) -> dict[str, Any]:
291
+ """
292
+ Dump model data with sensitive information masked.
293
+
294
+ Returns:
295
+ dict: Configuration with sensitive data masked
296
+ """
297
+ return self.model_dump()
298
+
299
+ @classmethod
300
+ def load_toml_config(cls, toml_path: Path) -> dict[str, Any]:
301
+ """Load configuration from a TOML file.
302
+
303
+ Args:
304
+ toml_path: Path to the TOML configuration file
305
+
306
+ Returns:
307
+ dict: Configuration data from the TOML file
308
+
309
+ Raises:
310
+ ValueError: If the TOML file is invalid or cannot be read
311
+ """
312
+ try:
313
+ with toml_path.open("rb") as f:
314
+ return tomllib.load(f)
315
+ except OSError as e:
316
+ raise ValueError(f"Cannot read TOML config file {toml_path}: {e}") from e
317
+ except tomllib.TOMLDecodeError as e:
318
+ raise ValueError(f"Invalid TOML syntax in {toml_path}: {e}") from e
319
+
320
+ @classmethod
321
+ def load_config_file(cls, config_path: Path) -> dict[str, Any]:
322
+ """Load configuration from a file based on its extension.
323
+
324
+ Args:
325
+ config_path: Path to the configuration file
326
+
327
+ Returns:
328
+ dict: Configuration data from the file
329
+
330
+ Raises:
331
+ ValueError: If the file format is unsupported or invalid
332
+ """
333
+ suffix = config_path.suffix.lower()
334
+
335
+ if suffix in [".toml"]:
336
+ return cls.load_toml_config(config_path)
337
+ else:
338
+ raise ValueError(
339
+ f"Unsupported config file format: {suffix}. "
340
+ "Only TOML (.toml) files are supported."
341
+ )
342
+
343
+ @classmethod
344
+ def from_toml(cls, toml_path: Path | None = None, **kwargs: Any) -> "Settings":
345
+ """Create Settings instance from TOML configuration.
346
+
347
+ Args:
348
+ toml_path: Path to TOML configuration file. If None, auto-discovers file.
349
+ **kwargs: Additional keyword arguments to override config values
350
+
351
+ Returns:
352
+ Settings: Configured Settings instance
353
+ """
354
+ # Use the more generic from_config method
355
+ return cls.from_config(config_path=toml_path, **kwargs)
356
+
357
+ @classmethod
358
+ def from_config(
359
+ cls, config_path: Path | str | None = None, **kwargs: Any
360
+ ) -> "Settings":
361
+ """Create Settings instance from configuration file.
362
+
363
+ Args:
364
+ config_path: Path to configuration file. Can be:
365
+ - None: Auto-discover config file or use CONFIG_FILE env var
366
+ - Path or str: Use this specific config file
367
+ **kwargs: Additional keyword arguments to override config values
368
+
369
+ Returns:
370
+ Settings: Configured Settings instance
371
+ """
372
+ # Check for CONFIG_FILE environment variable first
373
+ if config_path is None:
374
+ config_path_env = os.environ.get("CONFIG_FILE")
375
+ if config_path_env:
376
+ config_path = Path(config_path_env)
377
+
378
+ # Convert string to Path if needed
379
+ if isinstance(config_path, str):
380
+ config_path = Path(config_path)
381
+
382
+ # Auto-discover config file if not provided
383
+ if config_path is None:
384
+ config_path = find_toml_config_file()
385
+
386
+ # Load config if found
387
+ config_data = {}
388
+ if config_path and config_path.exists():
389
+ config_data = cls.load_config_file(config_path)
390
+
391
+ # Merge config with kwargs (kwargs take precedence)
392
+ merged_config = {**config_data, **kwargs}
393
+
394
+ # Create Settings instance with merged config
395
+ return cls(**merged_config)
396
+
397
+
398
+ class ConfigurationManager:
399
+ """Centralized configuration management for CLI and server."""
400
+
401
+ def __init__(self) -> None:
402
+ self._settings: Settings | None = None
403
+ self._config_path: Path | None = None
404
+ self._logging_configured = False
405
+
406
+ def load_settings(
407
+ self,
408
+ config_path: Path | None = None,
409
+ cli_overrides: dict[str, Any] | None = None,
410
+ ) -> Settings:
411
+ """Load settings with CLI overrides and caching."""
412
+ if self._settings is None or config_path != self._config_path:
413
+ try:
414
+ self._settings = Settings.from_config(
415
+ config_path=config_path, **(cli_overrides or {})
416
+ )
417
+ self._config_path = config_path
418
+ except Exception as e:
419
+ raise ConfigurationError(f"Failed to load configuration: {e}") from e
420
+
421
+ return self._settings
422
+
423
+ def setup_logging(self, log_level: str | None = None) -> None:
424
+ """Configure logging once based on settings."""
425
+ if self._logging_configured:
426
+ return
427
+
428
+ # Import here to avoid circular import
429
+
430
+ effective_level = log_level or (
431
+ self._settings.server.log_level if self._settings else "INFO"
432
+ )
433
+
434
+ # Determine format based on log level - Rich for DEBUG, JSON for production
435
+ format_type = "rich" if effective_level.upper() == "DEBUG" else "json"
436
+
437
+ # setup_dual_logging(
438
+ # level=effective_level,
439
+ # format_type=format_type,
440
+ # configure_uvicorn=True,
441
+ # verbose_tracebacks=effective_level.upper() == "DEBUG",
442
+ # )
443
+ self._logging_configured = True
444
+
445
+ def get_cli_overrides_from_args(self, **cli_args: Any) -> dict[str, Any]:
446
+ """Extract non-None CLI arguments as configuration overrides."""
447
+ overrides = {}
448
+
449
+ # Server settings
450
+ server_settings = {}
451
+ for key in ["host", "port", "reload", "log_level", "log_file"]:
452
+ if cli_args.get(key) is not None:
453
+ server_settings[key] = cli_args[key]
454
+ if server_settings:
455
+ overrides["server"] = server_settings
456
+
457
+ # Security settings
458
+ if cli_args.get("auth_token") is not None:
459
+ overrides["security"] = {"auth_token": cli_args["auth_token"]}
460
+
461
+ # Claude settings
462
+ claude_settings = {}
463
+ if cli_args.get("claude_cli_path") is not None:
464
+ claude_settings["cli_path"] = cli_args["claude_cli_path"]
465
+
466
+ # Claude Code options
467
+ claude_opts = {}
468
+ for key in [
469
+ "max_thinking_tokens",
470
+ "permission_mode",
471
+ "cwd",
472
+ "max_turns",
473
+ "append_system_prompt",
474
+ "permission_prompt_tool_name",
475
+ "continue_conversation",
476
+ ]:
477
+ if cli_args.get(key) is not None:
478
+ claude_opts[key] = cli_args[key]
479
+
480
+ # Handle comma-separated lists
481
+ for key in ["allowed_tools", "disallowed_tools"]:
482
+ if cli_args.get(key):
483
+ claude_opts[key] = [tool.strip() for tool in cli_args[key].split(",")]
484
+
485
+ if claude_opts:
486
+ claude_settings["code_options"] = claude_opts
487
+
488
+ if claude_settings:
489
+ overrides["claude"] = claude_settings
490
+
491
+ # CORS settings
492
+ if cli_args.get("cors_origins"):
493
+ overrides["cors"] = {
494
+ "origins": [
495
+ origin.strip() for origin in cli_args["cors_origins"].split(",")
496
+ ]
497
+ }
498
+
499
+ return overrides
500
+
501
+ def reset(self) -> None:
502
+ """Reset configuration state (useful for testing)."""
503
+ self._settings = None
504
+ self._config_path = None
505
+ self._logging_configured = False
506
+
507
+
508
+ # Global configuration manager instance
509
+ config_manager = ConfigurationManager()
510
+
511
+
512
+ def get_settings(config_path: Path | str | None = None) -> Settings:
513
+ """Get the global settings instance with configuration file support.
514
+
515
+ Args:
516
+ config_path: Optional path to configuration file. If None, uses CONFIG_FILE env var
517
+ or auto-discovers config file.
518
+
519
+ Returns:
520
+ Settings: Configured Settings instance
521
+ """
522
+ try:
523
+ # Check for CLI overrides from environment variable
524
+ cli_overrides = {}
525
+ cli_overrides_json = os.environ.get("CCPROXY_CONFIG_OVERRIDES")
526
+ if cli_overrides_json:
527
+ with contextlib.suppress(json.JSONDecodeError):
528
+ cli_overrides = json.loads(cli_overrides_json)
529
+
530
+ return Settings.from_config(config_path=config_path, **cli_overrides)
531
+ except Exception as e:
532
+ # If settings can't be loaded (e.g., missing API key),
533
+ # this will be handled by the caller
534
+ raise ValueError(f"Configuration error: {e}") from e