ccproxy-api 0.1.1__py3-none-any.whl → 0.1.3__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 (107) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/openai/__init__.py +1 -2
  3. ccproxy/adapters/openai/adapter.py +218 -180
  4. ccproxy/adapters/openai/streaming.py +247 -65
  5. ccproxy/api/__init__.py +0 -3
  6. ccproxy/api/app.py +173 -40
  7. ccproxy/api/dependencies.py +65 -3
  8. ccproxy/api/middleware/errors.py +3 -7
  9. ccproxy/api/middleware/headers.py +0 -2
  10. ccproxy/api/middleware/logging.py +4 -3
  11. ccproxy/api/middleware/request_content_logging.py +297 -0
  12. ccproxy/api/middleware/request_id.py +5 -0
  13. ccproxy/api/middleware/server_header.py +0 -4
  14. ccproxy/api/routes/__init__.py +9 -1
  15. ccproxy/api/routes/claude.py +23 -32
  16. ccproxy/api/routes/health.py +58 -4
  17. ccproxy/api/routes/mcp.py +171 -0
  18. ccproxy/api/routes/metrics.py +4 -8
  19. ccproxy/api/routes/permissions.py +217 -0
  20. ccproxy/api/routes/proxy.py +0 -53
  21. ccproxy/api/services/__init__.py +6 -0
  22. ccproxy/api/services/permission_service.py +368 -0
  23. ccproxy/api/ui/__init__.py +6 -0
  24. ccproxy/api/ui/permission_handler_protocol.py +33 -0
  25. ccproxy/api/ui/terminal_permission_handler.py +593 -0
  26. ccproxy/auth/conditional.py +2 -2
  27. ccproxy/auth/dependencies.py +1 -1
  28. ccproxy/auth/oauth/models.py +0 -1
  29. ccproxy/auth/oauth/routes.py +1 -3
  30. ccproxy/auth/storage/json_file.py +0 -1
  31. ccproxy/auth/storage/keyring.py +0 -3
  32. ccproxy/claude_sdk/__init__.py +2 -0
  33. ccproxy/claude_sdk/client.py +91 -8
  34. ccproxy/claude_sdk/converter.py +405 -210
  35. ccproxy/claude_sdk/options.py +88 -19
  36. ccproxy/claude_sdk/parser.py +200 -0
  37. ccproxy/claude_sdk/streaming.py +286 -0
  38. ccproxy/cli/commands/__init__.py +5 -1
  39. ccproxy/cli/commands/auth.py +2 -4
  40. ccproxy/cli/commands/permission_handler.py +553 -0
  41. ccproxy/cli/commands/serve.py +52 -12
  42. ccproxy/cli/docker/params.py +0 -4
  43. ccproxy/cli/helpers.py +0 -2
  44. ccproxy/cli/main.py +6 -17
  45. ccproxy/cli/options/claude_options.py +41 -1
  46. ccproxy/cli/options/core_options.py +0 -3
  47. ccproxy/cli/options/security_options.py +0 -2
  48. ccproxy/cli/options/server_options.py +3 -2
  49. ccproxy/config/auth.py +0 -1
  50. ccproxy/config/claude.py +78 -2
  51. ccproxy/config/discovery.py +0 -1
  52. ccproxy/config/docker_settings.py +0 -1
  53. ccproxy/config/loader.py +1 -4
  54. ccproxy/config/scheduler.py +20 -0
  55. ccproxy/config/security.py +7 -2
  56. ccproxy/config/server.py +5 -0
  57. ccproxy/config/settings.py +15 -7
  58. ccproxy/config/validators.py +1 -1
  59. ccproxy/core/async_utils.py +1 -4
  60. ccproxy/core/errors.py +45 -1
  61. ccproxy/core/http_transformers.py +4 -3
  62. ccproxy/core/interfaces.py +2 -2
  63. ccproxy/core/logging.py +97 -95
  64. ccproxy/core/middleware.py +1 -1
  65. ccproxy/core/proxy.py +1 -1
  66. ccproxy/core/transformers.py +1 -1
  67. ccproxy/core/types.py +1 -1
  68. ccproxy/docker/models.py +1 -1
  69. ccproxy/docker/protocol.py +0 -3
  70. ccproxy/models/__init__.py +41 -0
  71. ccproxy/models/claude_sdk.py +420 -0
  72. ccproxy/models/messages.py +45 -18
  73. ccproxy/models/permissions.py +115 -0
  74. ccproxy/models/requests.py +1 -1
  75. ccproxy/models/responses.py +64 -1
  76. ccproxy/observability/access_logger.py +1 -2
  77. ccproxy/observability/context.py +17 -1
  78. ccproxy/observability/metrics.py +1 -3
  79. ccproxy/observability/pushgateway.py +0 -2
  80. ccproxy/observability/stats_printer.py +2 -4
  81. ccproxy/observability/storage/duckdb_simple.py +1 -1
  82. ccproxy/observability/storage/models.py +0 -1
  83. ccproxy/pricing/cache.py +0 -1
  84. ccproxy/pricing/loader.py +5 -21
  85. ccproxy/pricing/updater.py +0 -1
  86. ccproxy/scheduler/__init__.py +1 -0
  87. ccproxy/scheduler/core.py +6 -6
  88. ccproxy/scheduler/manager.py +35 -7
  89. ccproxy/scheduler/registry.py +1 -1
  90. ccproxy/scheduler/tasks.py +127 -2
  91. ccproxy/services/claude_sdk_service.py +225 -329
  92. ccproxy/services/credentials/manager.py +0 -1
  93. ccproxy/services/credentials/oauth_client.py +1 -2
  94. ccproxy/services/proxy_service.py +93 -222
  95. ccproxy/testing/config.py +1 -1
  96. ccproxy/testing/mock_responses.py +0 -1
  97. ccproxy/utils/model_mapping.py +197 -0
  98. ccproxy/utils/models_provider.py +150 -0
  99. ccproxy/utils/simple_request_logger.py +284 -0
  100. ccproxy/utils/version_checker.py +184 -0
  101. {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
  102. ccproxy_api-0.1.3.dist-info/RECORD +166 -0
  103. {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +1 -0
  104. ccproxy_api-0.1.1.dist-info/RECORD +0 -149
  105. /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
  106. {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
  107. {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +10,6 @@ import uvicorn
10
10
  from click import get_current_context
11
11
  from structlog import get_logger
12
12
 
13
- from ccproxy._version import __version__
14
13
  from ccproxy.cli.helpers import (
15
14
  get_rich_toolkit,
16
15
  is_running_in_docker,
@@ -35,6 +34,8 @@ from ..options.claude_options import (
35
34
  validate_cwd,
36
35
  validate_max_thinking_tokens,
37
36
  validate_max_turns,
37
+ validate_permission_mode,
38
+ validate_sdk_message_mode,
38
39
  )
39
40
  from ..options.security_options import SecurityOptions, validate_auth_token
40
41
  from ..options.server_options import (
@@ -44,10 +45,6 @@ from ..options.server_options import (
44
45
  )
45
46
 
46
47
 
47
- # Logger will be configured by configuration manager
48
- logger = get_logger(__name__)
49
-
50
-
51
48
  def get_config_path_from_context() -> Path | None:
52
49
  """Get config path from typer context if available."""
53
50
  try:
@@ -327,7 +324,7 @@ def api(
327
324
  str | None,
328
325
  typer.Option(
329
326
  "--log-level",
330
- help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
327
+ help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Use WARNING for minimal output.",
331
328
  callback=validate_log_level,
332
329
  rich_help_panel="Server Settings",
333
330
  ),
@@ -340,6 +337,14 @@ def api(
340
337
  rich_help_panel="Server Settings",
341
338
  ),
342
339
  ] = None,
340
+ use_terminal_permission_handler: Annotated[
341
+ bool,
342
+ typer.Option(
343
+ "--terminal-permission-handler",
344
+ help="Enable terminal permission terminal handler",
345
+ rich_help_panel="Server Settings",
346
+ ),
347
+ ] = False,
343
348
  # Security options
344
349
  auth_token: Annotated[
345
350
  str | None,
@@ -393,6 +398,15 @@ def api(
393
398
  rich_help_panel="Claude Settings",
394
399
  ),
395
400
  ] = None,
401
+ permission_mode: Annotated[
402
+ str | None,
403
+ typer.Option(
404
+ "--permission-mode",
405
+ help="Permission mode: default, acceptEdits, or bypassPermissions",
406
+ callback=validate_permission_mode,
407
+ rich_help_panel="Claude Settings",
408
+ ),
409
+ ] = None,
396
410
  max_turns: Annotated[
397
411
  int | None,
398
412
  typer.Option(
@@ -411,6 +425,23 @@ def api(
411
425
  rich_help_panel="Claude Settings",
412
426
  ),
413
427
  ] = None,
428
+ permission_prompt_tool_name: Annotated[
429
+ str | None,
430
+ typer.Option(
431
+ "--permission-prompt-tool-name",
432
+ help="Permission prompt tool name",
433
+ rich_help_panel="Claude Settings",
434
+ ),
435
+ ] = None,
436
+ sdk_message_mode: Annotated[
437
+ str | None,
438
+ typer.Option(
439
+ "--sdk-message-mode",
440
+ help="SDK message handling mode: forward (direct SDK blocks), ignore (skip blocks), formatted (XML tags with JSON data)",
441
+ callback=validate_sdk_message_mode,
442
+ rich_help_panel="Claude Settings",
443
+ ),
444
+ ] = None,
414
445
  # Core settings
415
446
  docker: Annotated[
416
447
  bool,
@@ -528,6 +559,7 @@ def api(
528
559
  reload=reload,
529
560
  log_level=log_level,
530
561
  log_file=log_file,
562
+ use_terminal_confirmation_handler=use_terminal_permission_handler,
531
563
  )
532
564
 
533
565
  claude_options = ClaudeOptions(
@@ -536,8 +568,11 @@ def api(
536
568
  disallowed_tools=disallowed_tools,
537
569
  claude_cli_path=claude_cli_path,
538
570
  append_system_prompt=append_system_prompt,
571
+ permission_mode=permission_mode,
539
572
  max_turns=max_turns,
540
573
  cwd=cwd,
574
+ permission_prompt_tool_name=permission_prompt_tool_name,
575
+ sdk_message_mode=sdk_message_mode,
541
576
  )
542
577
 
543
578
  security_options = SecurityOptions(auth_token=auth_token)
@@ -550,6 +585,7 @@ def api(
550
585
  reload=server_options.reload,
551
586
  log_level=server_options.log_level,
552
587
  log_file=server_options.log_file,
588
+ use_terminal_confirmation_handler=server_options.use_terminal_confirmation_handler,
553
589
  # Security options
554
590
  auth_token=security_options.auth_token,
555
591
  # Claude options
@@ -558,8 +594,11 @@ def api(
558
594
  allowed_tools=claude_options.allowed_tools,
559
595
  disallowed_tools=claude_options.disallowed_tools,
560
596
  append_system_prompt=claude_options.append_system_prompt,
597
+ permission_mode=claude_options.permission_mode,
561
598
  max_turns=claude_options.max_turns,
599
+ permission_prompt_tool_name=claude_options.permission_prompt_tool_name,
562
600
  cwd=claude_options.cwd,
601
+ sdk_message_mode=claude_options.sdk_message_mode,
563
602
  )
564
603
 
565
604
  # Load settings with CLI overrides
@@ -569,17 +608,16 @@ def api(
569
608
 
570
609
  # Set up logging once with the effective log level
571
610
  # Import here to avoid circular import
572
- import structlog
573
611
 
574
612
  from ccproxy.core.logging import setup_logging
575
613
 
576
614
  # Always reconfigure logging to ensure log level changes are picked up
577
615
  # Use JSON logs if explicitly requested via env var
578
- json_logs = os.environ.get("CCPROXY_JSON_LOGS", "").lower() == "true"
616
+ print(f"{settings.server.log_level} {settings.server.log_file}")
579
617
  setup_logging(
580
- json_logs=json_logs,
581
- log_level=server_options.log_level or settings.server.log_level,
582
- log_file=server_options.log_file or settings.server.log_file,
618
+ json_logs=settings.server.log_format == "json",
619
+ log_level_name=settings.server.log_level,
620
+ log_file=settings.server.log_file,
583
621
  )
584
622
 
585
623
  # Re-get logger after logging is configured
@@ -602,7 +640,7 @@ def api(
602
640
  )
603
641
 
604
642
  # Log effective configuration
605
- logger.info(
643
+ logger.debug(
606
644
  "configuration_loaded",
607
645
  host=settings.server.host,
608
646
  port=settings.server.port,
@@ -756,6 +794,8 @@ def claude(
756
794
  toolkit = get_rich_toolkit()
757
795
 
758
796
  try:
797
+ # Logger will be configured by configuration manager
798
+ logger = get_logger(__name__)
759
799
  # Log CLI command execution start
760
800
  logger.info(
761
801
  "cli_command_starting",
@@ -82,8 +82,6 @@ def validate_docker_home(
82
82
  if value is None:
83
83
  return None
84
84
 
85
- from pathlib import Path
86
-
87
85
  from ccproxy.config.docker_settings import validate_host_path
88
86
 
89
87
  try:
@@ -116,8 +114,6 @@ def validate_docker_workspace(
116
114
  if value is None:
117
115
  return None
118
116
 
119
- from pathlib import Path
120
-
121
117
  from ccproxy.config.docker_settings import validate_host_path
122
118
 
123
119
  try:
ccproxy/cli/helpers.py CHANGED
@@ -1,12 +1,10 @@
1
1
  """CLI helper utilities for CCProxy API."""
2
2
 
3
- import logging
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
7
6
  from rich_toolkit import RichToolkit, RichToolkitTheme
8
7
  from rich_toolkit.styles import TaggedStyle
9
- from uvicorn.logging import DefaultFormatter
10
8
 
11
9
  from ccproxy.core.async_utils import patched_typing
12
10
 
ccproxy/cli/main.py CHANGED
@@ -1,34 +1,20 @@
1
1
  """Main entry point for CCProxy API Server."""
2
2
 
3
- import os
4
- import secrets
5
3
  from pathlib import Path
6
- from typing import Annotated, Any, Optional, cast
4
+ from typing import Annotated
7
5
 
8
6
  import typer
9
- from click import get_current_context
10
7
  from structlog import get_logger
11
- from typer import Typer
12
8
 
13
9
  from ccproxy._version import __version__
14
- from ccproxy.api.middleware.cors import setup_cors_middleware
15
10
  from ccproxy.cli.helpers import (
16
11
  get_rich_toolkit,
17
- is_running_in_docker,
18
- warning,
19
12
  )
20
- from ccproxy.config.settings import (
21
- ConfigurationError,
22
- Settings,
23
- config_manager,
24
- get_settings,
25
- )
26
- from ccproxy.core.async_utils import get_package_dir, get_root_package_name
27
- from ccproxy.core.logging import setup_logging
28
13
 
29
14
  from .commands.auth import app as auth_app
30
15
  from .commands.config import app as config_app
31
- from .commands.serve import api, get_config_path_from_context
16
+ from .commands.permission_handler import app as permission_handler_app
17
+ from .commands.serve import api
32
18
 
33
19
 
34
20
  def version_callback(value: bool) -> None:
@@ -98,6 +84,9 @@ app.add_typer(config_app)
98
84
  # Register auth command
99
85
  app.add_typer(auth_app)
100
86
 
87
+ # Register permission handler command
88
+ app.add_typer(permission_handler_app)
89
+
101
90
 
102
91
  # Register imported commands
103
92
  app.command(name="serve")(api)
@@ -1,7 +1,6 @@
1
1
  """Claude-specific CLI options."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
5
4
 
6
5
  import typer
7
6
 
@@ -32,6 +31,22 @@ def validate_max_turns(
32
31
  return value
33
32
 
34
33
 
34
+ def validate_permission_mode(
35
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
36
+ ) -> str | None:
37
+ """Validate permission mode."""
38
+ if value is None:
39
+ return None
40
+
41
+ valid_modes = {"default", "acceptEdits", "bypassPermissions"}
42
+ if value not in valid_modes:
43
+ raise typer.BadParameter(
44
+ f"Permission mode must be one of: {', '.join(valid_modes)}"
45
+ )
46
+
47
+ return value
48
+
49
+
35
50
  def validate_claude_cli_path(
36
51
  ctx: typer.Context, param: typer.CallbackParam, value: str | None
37
52
  ) -> str | None:
@@ -62,6 +77,22 @@ def validate_cwd(
62
77
  return value
63
78
 
64
79
 
80
+ def validate_sdk_message_mode(
81
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
82
+ ) -> str | None:
83
+ """Validate SDK message mode."""
84
+ if value is None:
85
+ return None
86
+
87
+ valid_modes = {"forward", "ignore", "formatted"}
88
+ if value not in valid_modes:
89
+ raise typer.BadParameter(
90
+ f"SDK message mode must be one of: {', '.join(valid_modes)}"
91
+ )
92
+
93
+ return value
94
+
95
+
65
96
  # Factory functions removed - use Annotated syntax directly in commands
66
97
 
67
98
 
@@ -79,8 +110,11 @@ class ClaudeOptions:
79
110
  disallowed_tools: str | None = None,
80
111
  claude_cli_path: str | None = None,
81
112
  append_system_prompt: str | None = None,
113
+ permission_mode: str | None = None,
82
114
  max_turns: int | None = None,
83
115
  cwd: str | None = None,
116
+ permission_prompt_tool_name: str | None = None,
117
+ sdk_message_mode: str | None = None,
84
118
  ):
85
119
  """Initialize Claude options.
86
120
 
@@ -90,13 +124,19 @@ class ClaudeOptions:
90
124
  disallowed_tools: List of disallowed tools (comma-separated)
91
125
  claude_cli_path: Path to Claude CLI executable
92
126
  append_system_prompt: Additional system prompt to append
127
+ permission_mode: Permission mode
93
128
  max_turns: Maximum conversation turns
94
129
  cwd: Working directory path
130
+ permission_prompt_tool_name: Permission prompt tool name
131
+ sdk_message_mode: SDK message handling mode
95
132
  """
96
133
  self.max_thinking_tokens = max_thinking_tokens
97
134
  self.allowed_tools = allowed_tools
98
135
  self.disallowed_tools = disallowed_tools
99
136
  self.claude_cli_path = claude_cli_path
100
137
  self.append_system_prompt = append_system_prompt
138
+ self.permission_mode = permission_mode
101
139
  self.max_turns = max_turns
102
140
  self.cwd = cwd
141
+ self.permission_prompt_tool_name = permission_prompt_tool_name
142
+ self.sdk_message_mode = sdk_message_mode
@@ -1,9 +1,6 @@
1
1
  """Core CLI options for configuration and global settings."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
5
-
6
- import typer
7
4
 
8
5
 
9
6
  # Factory functions removed - use Annotated syntax directly in commands
@@ -1,7 +1,5 @@
1
1
  """Security-related CLI options."""
2
2
 
3
- from typing import Any
4
-
5
3
  import typer
6
4
 
7
5
 
@@ -1,7 +1,5 @@
1
1
  """Server-related CLI options."""
2
2
 
3
- from typing import Any
4
-
5
3
  import typer
6
4
 
7
5
 
@@ -49,6 +47,7 @@ class ServerOptions:
49
47
  reload: bool | None = None,
50
48
  log_level: str | None = None,
51
49
  log_file: str | None = None,
50
+ use_terminal_confirmation_handler: bool | None = None,
52
51
  ):
53
52
  """Initialize server options.
54
53
 
@@ -58,9 +57,11 @@ class ServerOptions:
58
57
  reload: Enable auto-reload for development
59
58
  log_level: Logging level
60
59
  log_file: Path to JSON log file
60
+ use_terminal_confirmation_handler: Enable terminal UI for confirmation prompts
61
61
  """
62
62
  self.port = port
63
63
  self.host = host
64
64
  self.reload = reload
65
65
  self.log_level = log_level
66
66
  self.log_file = log_file
67
+ self.use_terminal_confirmation_handler = use_terminal_confirmation_handler
ccproxy/config/auth.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """Authentication and credentials configuration."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
ccproxy/config/claude.py CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  import os
4
4
  import shutil
5
+ from enum import Enum
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
9
+ import structlog
8
10
  from pydantic import BaseModel, Field, field_validator
9
11
 
10
12
  from ccproxy.core.async_utils import get_package_dir, patched_typing
@@ -14,6 +16,31 @@ from ccproxy.core.async_utils import get_package_dir, patched_typing
14
16
  with patched_typing():
15
17
  from claude_code_sdk import ClaudeCodeOptions # noqa: E402
16
18
 
19
+ logger = structlog.get_logger(__name__)
20
+
21
+
22
+ def _create_default_claude_code_options() -> ClaudeCodeOptions:
23
+ """Create ClaudeCodeOptions with default values."""
24
+ return ClaudeCodeOptions(
25
+ mcp_servers={
26
+ "confirmation": {"type": "sse", "url": "http://127.0.0.1:8000/mcp"}
27
+ },
28
+ permission_prompt_tool_name="mcp__confirmation__check_permission",
29
+ )
30
+
31
+
32
+ class SDKMessageMode(str, Enum):
33
+ """Modes for handling SDK messages from Claude SDK.
34
+
35
+ - forward: Forward SDK content blocks directly with original types and metadata
36
+ - ignore: Skip SDK messages and blocks completely
37
+ - formatted: Format as XML tags with JSON data in text deltas
38
+ """
39
+
40
+ FORWARD = "forward"
41
+ IGNORE = "ignore"
42
+ FORMATTED = "formatted"
43
+
17
44
 
18
45
  class ClaudeSettings(BaseModel):
19
46
  """Claude-specific configuration settings."""
@@ -24,10 +51,20 @@ class ClaudeSettings(BaseModel):
24
51
  )
25
52
 
26
53
  code_options: ClaudeCodeOptions = Field(
27
- default_factory=lambda: ClaudeCodeOptions(),
54
+ default_factory=_create_default_claude_code_options,
28
55
  description="Claude Code SDK options configuration",
29
56
  )
30
57
 
58
+ sdk_message_mode: SDKMessageMode = Field(
59
+ default=SDKMessageMode.FORWARD,
60
+ description="Mode for handling SDK messages from Claude SDK. Options: forward (direct SDK blocks), ignore (skip blocks), formatted (XML tags with JSON data)",
61
+ )
62
+
63
+ pretty_format: bool = Field(
64
+ default=True,
65
+ description="Whether to use pretty formatting (indented JSON, newlines after XML tags, unescaped content). When false: compact JSON, no newlines, escaped content between XML tags",
66
+ )
67
+
31
68
  @field_validator("cli_path")
32
69
  @classmethod
33
70
  def validate_claude_cli_path(cls, v: str | None) -> str | None:
@@ -47,12 +84,51 @@ class ClaudeSettings(BaseModel):
47
84
  def validate_claude_code_options(cls, v: Any) -> Any:
48
85
  """Validate and convert Claude Code options."""
49
86
  if v is None:
50
- return ClaudeCodeOptions()
87
+ # Create instance with default values (same as default_factory)
88
+ return _create_default_claude_code_options()
51
89
 
52
90
  # If it's already a ClaudeCodeOptions instance, return as-is
53
91
  if isinstance(v, ClaudeCodeOptions):
54
92
  return v
55
93
 
94
+ # If it's an empty dict, treat it like None and use defaults
95
+ if isinstance(v, dict) and not v:
96
+ return _create_default_claude_code_options()
97
+
98
+ # For non-empty dicts, merge with defaults instead of replacing them
99
+ if isinstance(v, dict):
100
+ # Start with default values
101
+ defaults = _create_default_claude_code_options()
102
+
103
+ # Extract default values as a dict for merging
104
+ default_values = {
105
+ "mcp_servers": defaults.mcp_servers.copy(),
106
+ "permission_prompt_tool_name": defaults.permission_prompt_tool_name,
107
+ }
108
+
109
+ # Add other default attributes if they exist
110
+ for attr in [
111
+ "max_thinking_tokens",
112
+ "allowed_tools",
113
+ "disallowed_tools",
114
+ "cwd",
115
+ "append_system_prompt",
116
+ "max_turns",
117
+ "continue_conversation",
118
+ "permission_mode",
119
+ "model",
120
+ "system_prompt",
121
+ ]:
122
+ if hasattr(defaults, attr):
123
+ default_value = getattr(defaults, attr, None)
124
+ if default_value is not None:
125
+ default_values[attr] = default_value
126
+
127
+ # Merge CLI overrides with defaults (CLI overrides take precedence)
128
+ merged_values = {**default_values, **v}
129
+
130
+ return ClaudeCodeOptions(**merged_values)
131
+
56
132
  # Try to convert to dict if possible
57
133
  if hasattr(v, "model_dump"):
58
134
  return v.model_dump()
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
 
4
3
  from ccproxy.core.system import get_xdg_config_home
5
4
 
@@ -1,7 +1,6 @@
1
1
  """Docker settings configuration for CCProxy API."""
2
2
 
3
3
  import os
4
- from typing import Any
5
4
 
6
5
  from pydantic import BaseModel, Field, field_validator, model_validator
7
6
 
ccproxy/config/loader.py CHANGED
@@ -1,10 +1,7 @@
1
1
  """Configuration file loader for ccproxy."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
- from typing import Any, Optional
6
-
7
- from pydantic import BaseModel
4
+ from typing import Any
8
5
 
9
6
  from ccproxy.config.discovery import find_toml_config_file
10
7
  from ccproxy.config.settings import Settings
@@ -82,6 +82,26 @@ class SchedulerSettings(BaseSettings):
82
82
  description="Interval in seconds between stats printing",
83
83
  )
84
84
 
85
+ # Version checking task settings
86
+ version_check_enabled: bool = Field(
87
+ default=True,
88
+ description="Whether version update checking is enabled",
89
+ )
90
+
91
+ version_check_interval_hours: int = Field(
92
+ default=12,
93
+ ge=1,
94
+ le=168, # Max 1 week
95
+ description="Interval in hours between version checks",
96
+ )
97
+
98
+ version_check_startup_max_age_hours: float = Field(
99
+ default=1.0,
100
+ ge=0.1,
101
+ le=24.0,
102
+ description="Maximum age in hours since last check before running startup check",
103
+ )
104
+
85
105
  model_config = SettingsConfigDict(
86
106
  env_prefix="SCHEDULER__",
87
107
  case_sensitive=False,
@@ -1,7 +1,5 @@
1
1
  """Security configuration settings."""
2
2
 
3
- from typing import Literal
4
-
5
3
  from pydantic import BaseModel, Field
6
4
 
7
5
 
@@ -12,3 +10,10 @@ class SecuritySettings(BaseModel):
12
10
  default=None,
13
11
  description="Bearer token for API authentication (optional)",
14
12
  )
13
+
14
+ confirmation_timeout_seconds: int = Field(
15
+ default=30,
16
+ ge=5,
17
+ le=300,
18
+ description="Timeout in seconds for permission confirmation requests (5-300)",
19
+ )
ccproxy/config/server.py CHANGED
@@ -60,6 +60,11 @@ class ServerSettings(BaseModel):
60
60
  description="Path to JSON log file. If specified, logs will be written to this file in JSON format",
61
61
  )
62
62
 
63
+ use_terminal_permission_handler: bool = Field(
64
+ default=False,
65
+ description="Enable terminal UI for permission prompts. Set to False to use external handler via SSE (not implemented)",
66
+ )
67
+
63
68
  @field_validator("log_level")
64
69
  @classmethod
65
70
  def validate_log_level(cls, v: str) -> str:
@@ -3,17 +3,15 @@
3
3
  import contextlib
4
4
  import json
5
5
  import os
6
- import shutil
7
6
  import tomllib
8
7
  from pathlib import Path
9
- from typing import Any, Literal
8
+ from typing import Any
10
9
 
11
- from pydantic import BaseModel, Field, field_validator, model_validator
10
+ import structlog
11
+ from pydantic import Field, field_validator, model_validator
12
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
13
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
14
+ from ccproxy.config.discovery import find_toml_config_file
17
15
 
18
16
  from .auth import AuthSettings
19
17
  from .claude import ClaudeSettings
@@ -463,13 +461,20 @@ class ConfigurationManager:
463
461
  if cli_args.get("claude_cli_path") is not None:
464
462
  claude_settings["cli_path"] = cli_args["claude_cli_path"]
465
463
 
464
+ # Direct Claude settings (not nested in code_options)
465
+ for key in ["sdk_message_mode"]:
466
+ if cli_args.get(key) is not None:
467
+ claude_settings[key] = cli_args[key]
468
+
466
469
  # Claude Code options
467
470
  claude_opts = {}
468
471
  for key in [
469
472
  "max_thinking_tokens",
473
+ "permission_mode",
470
474
  "cwd",
471
475
  "max_turns",
472
476
  "append_system_prompt",
477
+ "permission_prompt_tool_name",
473
478
  "continue_conversation",
474
479
  ]:
475
480
  if cli_args.get(key) is not None:
@@ -506,6 +511,8 @@ class ConfigurationManager:
506
511
  # Global configuration manager instance
507
512
  config_manager = ConfigurationManager()
508
513
 
514
+ logger = structlog.get_logger(__name__)
515
+
509
516
 
510
517
  def get_settings(config_path: Path | str | None = None) -> Settings:
511
518
  """Get the global settings instance with configuration file support.
@@ -525,7 +532,8 @@ def get_settings(config_path: Path | str | None = None) -> Settings:
525
532
  with contextlib.suppress(json.JSONDecodeError):
526
533
  cli_overrides = json.loads(cli_overrides_json)
527
534
 
528
- return Settings.from_config(config_path=config_path, **cli_overrides)
535
+ settings = Settings.from_config(config_path=config_path, **cli_overrides)
536
+ return settings
529
537
  except Exception as e:
530
538
  # If settings can't be loaded (e.g., missing API key),
531
539
  # this will be handled by the caller
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import Any, Optional, Union
5
+ from typing import Any
6
6
  from urllib.parse import urlparse
7
7
 
8
8
 
@@ -465,15 +465,12 @@ def validate_config_with_schema(
465
465
  import json
466
466
  import subprocess
467
467
  import tempfile
468
- from typing import Any
469
468
 
470
469
  # Import tomllib for Python 3.11+ or fallback to tomli
471
470
  try:
472
471
  import tomllib
473
472
  except ImportError:
474
- import tomli as tomllib # type: ignore[import-not-found,no-redef]
475
-
476
- from ccproxy.config.settings import Settings
473
+ import tomli as tomllib # type: ignore[no-redef]
477
474
 
478
475
  config_path = Path()
479
476