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.
- ccproxy/_version.py +2 -2
- ccproxy/adapters/openai/__init__.py +1 -2
- ccproxy/adapters/openai/adapter.py +218 -180
- ccproxy/adapters/openai/streaming.py +247 -65
- ccproxy/api/__init__.py +0 -3
- ccproxy/api/app.py +173 -40
- ccproxy/api/dependencies.py +65 -3
- ccproxy/api/middleware/errors.py +3 -7
- ccproxy/api/middleware/headers.py +0 -2
- ccproxy/api/middleware/logging.py +4 -3
- ccproxy/api/middleware/request_content_logging.py +297 -0
- ccproxy/api/middleware/request_id.py +5 -0
- ccproxy/api/middleware/server_header.py +0 -4
- ccproxy/api/routes/__init__.py +9 -1
- ccproxy/api/routes/claude.py +23 -32
- ccproxy/api/routes/health.py +58 -4
- ccproxy/api/routes/mcp.py +171 -0
- ccproxy/api/routes/metrics.py +4 -8
- ccproxy/api/routes/permissions.py +217 -0
- ccproxy/api/routes/proxy.py +0 -53
- ccproxy/api/services/__init__.py +6 -0
- ccproxy/api/services/permission_service.py +368 -0
- ccproxy/api/ui/__init__.py +6 -0
- ccproxy/api/ui/permission_handler_protocol.py +33 -0
- ccproxy/api/ui/terminal_permission_handler.py +593 -0
- ccproxy/auth/conditional.py +2 -2
- ccproxy/auth/dependencies.py +1 -1
- ccproxy/auth/oauth/models.py +0 -1
- ccproxy/auth/oauth/routes.py +1 -3
- ccproxy/auth/storage/json_file.py +0 -1
- ccproxy/auth/storage/keyring.py +0 -3
- ccproxy/claude_sdk/__init__.py +2 -0
- ccproxy/claude_sdk/client.py +91 -8
- ccproxy/claude_sdk/converter.py +405 -210
- ccproxy/claude_sdk/options.py +88 -19
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -1
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +52 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +6 -17
- ccproxy/cli/options/claude_options.py +41 -1
- ccproxy/cli/options/core_options.py +0 -3
- ccproxy/cli/options/security_options.py +0 -2
- ccproxy/cli/options/server_options.py +3 -2
- ccproxy/config/auth.py +0 -1
- ccproxy/config/claude.py +78 -2
- ccproxy/config/discovery.py +0 -1
- ccproxy/config/docker_settings.py +0 -1
- ccproxy/config/loader.py +1 -4
- ccproxy/config/scheduler.py +20 -0
- ccproxy/config/security.py +7 -2
- ccproxy/config/server.py +5 -0
- ccproxy/config/settings.py +15 -7
- ccproxy/config/validators.py +1 -1
- ccproxy/core/async_utils.py +1 -4
- ccproxy/core/errors.py +45 -1
- ccproxy/core/http_transformers.py +4 -3
- ccproxy/core/interfaces.py +2 -2
- ccproxy/core/logging.py +97 -95
- ccproxy/core/middleware.py +1 -1
- ccproxy/core/proxy.py +1 -1
- ccproxy/core/transformers.py +1 -1
- ccproxy/core/types.py +1 -1
- ccproxy/docker/models.py +1 -1
- ccproxy/docker/protocol.py +0 -3
- ccproxy/models/__init__.py +41 -0
- ccproxy/models/claude_sdk.py +420 -0
- ccproxy/models/messages.py +45 -18
- ccproxy/models/permissions.py +115 -0
- ccproxy/models/requests.py +1 -1
- ccproxy/models/responses.py +64 -1
- ccproxy/observability/access_logger.py +1 -2
- ccproxy/observability/context.py +17 -1
- ccproxy/observability/metrics.py +1 -3
- ccproxy/observability/pushgateway.py +0 -2
- ccproxy/observability/stats_printer.py +2 -4
- ccproxy/observability/storage/duckdb_simple.py +1 -1
- ccproxy/observability/storage/models.py +0 -1
- ccproxy/pricing/cache.py +0 -1
- ccproxy/pricing/loader.py +5 -21
- ccproxy/pricing/updater.py +0 -1
- ccproxy/scheduler/__init__.py +1 -0
- ccproxy/scheduler/core.py +6 -6
- ccproxy/scheduler/manager.py +35 -7
- ccproxy/scheduler/registry.py +1 -1
- ccproxy/scheduler/tasks.py +127 -2
- ccproxy/services/claude_sdk_service.py +225 -329
- ccproxy/services/credentials/manager.py +0 -1
- ccproxy/services/credentials/oauth_client.py +1 -2
- ccproxy/services/proxy_service.py +93 -222
- ccproxy/testing/config.py +1 -1
- ccproxy/testing/mock_responses.py +0 -1
- ccproxy/utils/model_mapping.py +197 -0
- ccproxy/utils/models_provider.py +150 -0
- ccproxy/utils/simple_request_logger.py +284 -0
- ccproxy/utils/version_checker.py +184 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.3.dist-info/RECORD +166 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +1 -0
- ccproxy_api-0.1.1.dist-info/RECORD +0 -149
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
ccproxy/cli/commands/serve.py
CHANGED
|
@@ -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
|
-
|
|
616
|
+
print(f"{settings.server.log_level} {settings.server.log_file}")
|
|
579
617
|
setup_logging(
|
|
580
|
-
json_logs=
|
|
581
|
-
|
|
582
|
-
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.
|
|
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",
|
ccproxy/cli/docker/params.py
CHANGED
|
@@ -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
|
|
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.
|
|
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,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
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=
|
|
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
|
-
|
|
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()
|
ccproxy/config/discovery.py
CHANGED
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
|
|
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
|
ccproxy/config/scheduler.py
CHANGED
|
@@ -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,
|
ccproxy/config/security.py
CHANGED
|
@@ -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:
|
ccproxy/config/settings.py
CHANGED
|
@@ -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
|
|
8
|
+
from typing import Any
|
|
10
9
|
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
ccproxy/config/validators.py
CHANGED
ccproxy/core/async_utils.py
CHANGED
|
@@ -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[
|
|
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
|
|