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.
- ccproxy/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Docker adapter factory for CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides functions to create Docker adapters from CLI settings
|
|
4
|
+
and command-line arguments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import getpass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ccproxy.config.settings import Settings
|
|
12
|
+
from ccproxy.docker import (
|
|
13
|
+
DockerEnv,
|
|
14
|
+
DockerPath,
|
|
15
|
+
DockerUserContext,
|
|
16
|
+
DockerVolume,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _create_docker_adapter_from_settings(
|
|
21
|
+
settings: Settings,
|
|
22
|
+
docker_image: str | None = None,
|
|
23
|
+
docker_env: list[str] | None = None,
|
|
24
|
+
docker_volume: list[str] | None = None,
|
|
25
|
+
docker_arg: list[str] | None = None,
|
|
26
|
+
docker_home: str | None = None,
|
|
27
|
+
docker_workspace: str | None = None,
|
|
28
|
+
user_mapping_enabled: bool | None = None,
|
|
29
|
+
user_uid: int | None = None,
|
|
30
|
+
user_gid: int | None = None,
|
|
31
|
+
command: list[str] | None = None,
|
|
32
|
+
cmd_args: list[str] | None = None,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
) -> tuple[
|
|
35
|
+
str,
|
|
36
|
+
list[DockerVolume],
|
|
37
|
+
DockerEnv,
|
|
38
|
+
list[str] | None,
|
|
39
|
+
DockerUserContext | None,
|
|
40
|
+
list[str],
|
|
41
|
+
]:
|
|
42
|
+
"""Convert settings and overrides to Docker adapter parameters.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
settings: Application settings
|
|
46
|
+
docker_image: Override Docker image
|
|
47
|
+
docker_env: Additional environment variables
|
|
48
|
+
docker_volume: Additional volume mappings
|
|
49
|
+
docker_arg: Additional Docker arguments
|
|
50
|
+
docker_home: Override home directory
|
|
51
|
+
docker_workspace: Override workspace directory
|
|
52
|
+
user_mapping_enabled: Override user mapping setting
|
|
53
|
+
user_uid: Override user ID
|
|
54
|
+
user_gid: Override group ID
|
|
55
|
+
command: Command to run in container
|
|
56
|
+
cmd_args: Arguments for the command
|
|
57
|
+
**kwargs: Additional keyword arguments (ignored)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Tuple of (image, volumes, environment, command, user_context, additional_args)
|
|
61
|
+
"""
|
|
62
|
+
docker_settings = settings.docker
|
|
63
|
+
|
|
64
|
+
# Determine effective image
|
|
65
|
+
image = docker_image or docker_settings.docker_image
|
|
66
|
+
|
|
67
|
+
# Process volumes
|
|
68
|
+
volumes: list[DockerVolume] = []
|
|
69
|
+
|
|
70
|
+
# Add home/workspace volumes with effective directories
|
|
71
|
+
home_dir = docker_home or docker_settings.docker_home_directory
|
|
72
|
+
workspace_dir = docker_workspace or docker_settings.docker_workspace_directory
|
|
73
|
+
|
|
74
|
+
if home_dir:
|
|
75
|
+
volumes.append((str(Path(home_dir)), "/data/home"))
|
|
76
|
+
if workspace_dir:
|
|
77
|
+
volumes.append((str(Path(workspace_dir)), "/data/workspace"))
|
|
78
|
+
|
|
79
|
+
# Add base volumes from settings
|
|
80
|
+
for vol_str in docker_settings.docker_volumes:
|
|
81
|
+
parts = vol_str.split(":", 2)
|
|
82
|
+
if len(parts) >= 2:
|
|
83
|
+
volumes.append((parts[0], parts[1]))
|
|
84
|
+
|
|
85
|
+
# Add CLI override volumes
|
|
86
|
+
if docker_volume:
|
|
87
|
+
for vol_str in docker_volume:
|
|
88
|
+
parts = vol_str.split(":", 2)
|
|
89
|
+
if len(parts) >= 2:
|
|
90
|
+
volumes.append((parts[0], parts[1]))
|
|
91
|
+
|
|
92
|
+
# Process environment
|
|
93
|
+
environment: DockerEnv = docker_settings.docker_environment.copy()
|
|
94
|
+
|
|
95
|
+
# Add home/workspace environment variables
|
|
96
|
+
if home_dir:
|
|
97
|
+
environment["CLAUDE_HOME"] = "/data/home"
|
|
98
|
+
if workspace_dir:
|
|
99
|
+
environment["CLAUDE_WORKSPACE"] = "/data/workspace"
|
|
100
|
+
|
|
101
|
+
# Add CLI override environment
|
|
102
|
+
if docker_env:
|
|
103
|
+
for env_var in docker_env:
|
|
104
|
+
if "=" in env_var:
|
|
105
|
+
key, value = env_var.split("=", 1)
|
|
106
|
+
environment[key] = value
|
|
107
|
+
|
|
108
|
+
# Create user context
|
|
109
|
+
user_context = None
|
|
110
|
+
effective_mapping_enabled = (
|
|
111
|
+
user_mapping_enabled
|
|
112
|
+
if user_mapping_enabled is not None
|
|
113
|
+
else docker_settings.user_mapping_enabled
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if effective_mapping_enabled:
|
|
117
|
+
effective_uid = user_uid if user_uid is not None else docker_settings.user_uid
|
|
118
|
+
effective_gid = user_gid if user_gid is not None else docker_settings.user_gid
|
|
119
|
+
|
|
120
|
+
if effective_uid is not None and effective_gid is not None:
|
|
121
|
+
# Create DockerPath instances for user context
|
|
122
|
+
home_path = None
|
|
123
|
+
workspace_path = None
|
|
124
|
+
|
|
125
|
+
if home_dir:
|
|
126
|
+
home_path = DockerPath(
|
|
127
|
+
host_path=Path(home_dir), container_path="/data/home"
|
|
128
|
+
)
|
|
129
|
+
if workspace_dir:
|
|
130
|
+
workspace_path = DockerPath(
|
|
131
|
+
host_path=Path(workspace_dir), container_path="/data/workspace"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Use a default username if not available
|
|
135
|
+
username = getpass.getuser()
|
|
136
|
+
|
|
137
|
+
user_context = DockerUserContext(
|
|
138
|
+
uid=effective_uid,
|
|
139
|
+
gid=effective_gid,
|
|
140
|
+
username=username,
|
|
141
|
+
home_path=home_path,
|
|
142
|
+
workspace_path=workspace_path,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Build command
|
|
146
|
+
final_command = None
|
|
147
|
+
if command:
|
|
148
|
+
final_command = command.copy()
|
|
149
|
+
if cmd_args:
|
|
150
|
+
final_command.extend(cmd_args)
|
|
151
|
+
|
|
152
|
+
# Additional Docker arguments
|
|
153
|
+
additional_args = docker_settings.docker_additional_args.copy()
|
|
154
|
+
if docker_arg:
|
|
155
|
+
additional_args.extend(docker_arg)
|
|
156
|
+
|
|
157
|
+
return image, volumes, environment, final_command, user_context, additional_args
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Shared Docker parameter definitions for Typer CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides reusable Typer Option definitions for Docker-related
|
|
4
|
+
parameters that are used across multiple CLI commands, eliminating duplication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Docker parameter validation functions moved here to avoid utils dependency
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_docker_env(
|
|
16
|
+
ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
|
|
17
|
+
) -> list[str]:
|
|
18
|
+
"""Parse Docker environment variable string."""
|
|
19
|
+
if not value:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
parsed = []
|
|
23
|
+
for env_str in value:
|
|
24
|
+
if not env_str or env_str == "[]":
|
|
25
|
+
raise typer.BadParameter(
|
|
26
|
+
f"Invalid env format: {env_str}. Expected KEY=VALUE"
|
|
27
|
+
)
|
|
28
|
+
if "=" not in env_str:
|
|
29
|
+
raise typer.BadParameter(
|
|
30
|
+
f"Invalid env format: {env_str}. Expected KEY=VALUE"
|
|
31
|
+
)
|
|
32
|
+
parsed.append(env_str)
|
|
33
|
+
|
|
34
|
+
return parsed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_docker_volume(
|
|
38
|
+
ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
|
|
39
|
+
) -> list[str]:
|
|
40
|
+
"""Parse Docker volume string."""
|
|
41
|
+
if not value:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
# Import the validation function from config
|
|
45
|
+
from ccproxy.config.docker_settings import validate_volume_format
|
|
46
|
+
|
|
47
|
+
parsed = []
|
|
48
|
+
for volume_str in value:
|
|
49
|
+
if not volume_str:
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
validated_volume = validate_volume_format(volume_str)
|
|
53
|
+
parsed.append(validated_volume)
|
|
54
|
+
except ValueError as e:
|
|
55
|
+
raise typer.BadParameter(str(e)) from e
|
|
56
|
+
|
|
57
|
+
return parsed
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_docker_arg(
|
|
61
|
+
ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
|
|
62
|
+
) -> list[str]:
|
|
63
|
+
"""Validate Docker argument."""
|
|
64
|
+
if not value:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
# Basic validation - ensure arguments don't contain dangerous patterns
|
|
68
|
+
validated = []
|
|
69
|
+
for arg in value:
|
|
70
|
+
if not arg:
|
|
71
|
+
continue
|
|
72
|
+
# Basic validation - just return the arg for now
|
|
73
|
+
validated.append(arg)
|
|
74
|
+
|
|
75
|
+
return validated
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_docker_home(
|
|
79
|
+
ctx: typer.Context, param: typer.CallbackParam, value: str | None
|
|
80
|
+
) -> str | None:
|
|
81
|
+
"""Validate Docker home directory."""
|
|
82
|
+
if value is None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
|
|
87
|
+
from ccproxy.config.docker_settings import validate_host_path
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
return validate_host_path(value)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
raise typer.BadParameter(str(e)) from e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_docker_image(
|
|
96
|
+
ctx: typer.Context, param: typer.CallbackParam, value: str | None
|
|
97
|
+
) -> str | None:
|
|
98
|
+
"""Validate Docker image name."""
|
|
99
|
+
if value is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
if not value:
|
|
103
|
+
raise typer.BadParameter("Docker image cannot be empty")
|
|
104
|
+
|
|
105
|
+
# Basic validation - no spaces allowed in image names
|
|
106
|
+
if " " in value:
|
|
107
|
+
raise typer.BadParameter(f"Docker image name cannot contain spaces: {value}")
|
|
108
|
+
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_docker_workspace(
|
|
113
|
+
ctx: typer.Context, param: typer.CallbackParam, value: str | None
|
|
114
|
+
) -> str | None:
|
|
115
|
+
"""Validate Docker workspace directory."""
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
from ccproxy.config.docker_settings import validate_host_path
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
return validate_host_path(value)
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
raise typer.BadParameter(str(e)) from e
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_user_gid(
|
|
130
|
+
ctx: typer.Context, param: typer.CallbackParam, value: int | None
|
|
131
|
+
) -> int | None:
|
|
132
|
+
"""Validate user GID."""
|
|
133
|
+
if value is None:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if value < 0:
|
|
137
|
+
raise typer.BadParameter("GID must be non-negative")
|
|
138
|
+
|
|
139
|
+
return value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_user_uid(
|
|
143
|
+
ctx: typer.Context, param: typer.CallbackParam, value: int | None
|
|
144
|
+
) -> int | None:
|
|
145
|
+
"""Validate user UID."""
|
|
146
|
+
if value is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
if value < 0:
|
|
150
|
+
raise typer.BadParameter("UID must be non-negative")
|
|
151
|
+
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def docker_image_option() -> Any:
|
|
156
|
+
"""Docker image parameter."""
|
|
157
|
+
return typer.Option(
|
|
158
|
+
None,
|
|
159
|
+
"--docker-image",
|
|
160
|
+
help="Docker image to use (overrides config)",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def docker_env_option() -> Any:
|
|
165
|
+
"""Docker environment variables parameter."""
|
|
166
|
+
return typer.Option(
|
|
167
|
+
[],
|
|
168
|
+
"--docker-env",
|
|
169
|
+
help="Environment variables to pass to Docker (KEY=VALUE format, can be used multiple times)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def docker_volume_option() -> Any:
|
|
174
|
+
"""Docker volume mounts parameter."""
|
|
175
|
+
return typer.Option(
|
|
176
|
+
[],
|
|
177
|
+
"--docker-volume",
|
|
178
|
+
help="Volume mounts to add (host:container[:options] format, can be used multiple times)",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def docker_arg_option() -> Any:
|
|
183
|
+
"""Docker arguments parameter."""
|
|
184
|
+
return typer.Option(
|
|
185
|
+
[],
|
|
186
|
+
"--docker-arg",
|
|
187
|
+
help="Additional Docker run arguments (can be used multiple times)",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def docker_home_option() -> Any:
|
|
192
|
+
"""Docker home directory parameter."""
|
|
193
|
+
return typer.Option(
|
|
194
|
+
None,
|
|
195
|
+
"--docker-home",
|
|
196
|
+
help="Home directory inside Docker container (overrides config)",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def docker_workspace_option() -> Any:
|
|
201
|
+
"""Docker workspace directory parameter."""
|
|
202
|
+
return typer.Option(
|
|
203
|
+
None,
|
|
204
|
+
"--docker-workspace",
|
|
205
|
+
help="Workspace directory inside Docker container (overrides config)",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def user_mapping_option() -> Any:
|
|
210
|
+
"""User mapping parameter."""
|
|
211
|
+
return typer.Option(
|
|
212
|
+
None,
|
|
213
|
+
"--user-mapping/--no-user-mapping",
|
|
214
|
+
help="Enable/disable UID/GID mapping (overrides config)",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def user_uid_option() -> Any:
|
|
219
|
+
"""User UID parameter."""
|
|
220
|
+
return typer.Option(
|
|
221
|
+
None,
|
|
222
|
+
"--user-uid",
|
|
223
|
+
help="User ID to run container as (overrides config)",
|
|
224
|
+
min=0,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def user_gid_option() -> Any:
|
|
229
|
+
"""User GID parameter."""
|
|
230
|
+
return typer.Option(
|
|
231
|
+
None,
|
|
232
|
+
"--user-gid",
|
|
233
|
+
help="Group ID to run container as (overrides config)",
|
|
234
|
+
min=0,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class DockerOptions:
|
|
239
|
+
"""Container for all Docker-related Typer options.
|
|
240
|
+
|
|
241
|
+
This class provides a convenient way to include all Docker-related
|
|
242
|
+
options in a command using typed attributes.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def __init__(
|
|
246
|
+
self,
|
|
247
|
+
docker_image: str | None = None,
|
|
248
|
+
docker_env: list[str] | None = None,
|
|
249
|
+
docker_volume: list[str] | None = None,
|
|
250
|
+
docker_arg: list[str] | None = None,
|
|
251
|
+
docker_home: str | None = None,
|
|
252
|
+
docker_workspace: str | None = None,
|
|
253
|
+
user_mapping_enabled: bool | None = None,
|
|
254
|
+
user_uid: int | None = None,
|
|
255
|
+
user_gid: int | None = None,
|
|
256
|
+
):
|
|
257
|
+
"""Initialize Docker options.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
docker_image: Docker image to use
|
|
261
|
+
docker_env: Environment variables list
|
|
262
|
+
docker_volume: Volume mounts list
|
|
263
|
+
docker_arg: Additional Docker arguments
|
|
264
|
+
docker_home: Home directory path
|
|
265
|
+
docker_workspace: Workspace directory path
|
|
266
|
+
user_mapping_enabled: User mapping flag
|
|
267
|
+
user_uid: User ID
|
|
268
|
+
user_gid: Group ID
|
|
269
|
+
"""
|
|
270
|
+
self.docker_image = docker_image
|
|
271
|
+
self.docker_env = docker_env or []
|
|
272
|
+
self.docker_volume = docker_volume or []
|
|
273
|
+
self.docker_arg = docker_arg or []
|
|
274
|
+
self.docker_home = docker_home
|
|
275
|
+
self.docker_workspace = docker_workspace
|
|
276
|
+
self.user_mapping_enabled = user_mapping_enabled
|
|
277
|
+
self.user_uid = user_uid
|
|
278
|
+
self.user_gid = user_gid
|
ccproxy/cli/helpers.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""CLI helper utilities for CCProxy API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich_toolkit import RichToolkit, RichToolkitTheme
|
|
8
|
+
from rich_toolkit.styles import TaggedStyle
|
|
9
|
+
from uvicorn.logging import DefaultFormatter
|
|
10
|
+
|
|
11
|
+
from ccproxy.core.async_utils import patched_typing
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_rich_toolkit() -> RichToolkit:
|
|
15
|
+
theme = RichToolkitTheme(
|
|
16
|
+
style=TaggedStyle(tag_width=11),
|
|
17
|
+
theme={
|
|
18
|
+
# Core tags
|
|
19
|
+
"tag.title": "white on #009485",
|
|
20
|
+
"tag": "white on #007166",
|
|
21
|
+
"placeholder": "grey85",
|
|
22
|
+
"text": "white",
|
|
23
|
+
"selected": "#007166",
|
|
24
|
+
"result": "grey85",
|
|
25
|
+
"progress": "on #007166",
|
|
26
|
+
# Status tags
|
|
27
|
+
"error": "bold red",
|
|
28
|
+
"success": "bold green",
|
|
29
|
+
"warning": "bold yellow",
|
|
30
|
+
"info": "blue",
|
|
31
|
+
# CLI specific tags
|
|
32
|
+
"version": "cyan",
|
|
33
|
+
"docker": "blue",
|
|
34
|
+
"local": "green",
|
|
35
|
+
"claude": "magenta",
|
|
36
|
+
"config": "cyan",
|
|
37
|
+
"volume": "yellow",
|
|
38
|
+
"env": "bright_blue",
|
|
39
|
+
"debug": "dim white",
|
|
40
|
+
"command": "bright_cyan",
|
|
41
|
+
# Logging
|
|
42
|
+
"log.info": "black on blue",
|
|
43
|
+
"log.error": "white on red",
|
|
44
|
+
"log.warning": "black on yellow",
|
|
45
|
+
"log.debug": "dim white",
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return RichToolkit(theme=theme)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def bold(text: str) -> str:
|
|
53
|
+
return f"[bold]{text}[/bold]"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dim(text: str) -> str:
|
|
57
|
+
return f"[dim]{text}[/dim]"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def italic(text: str) -> str:
|
|
61
|
+
return f"[italic]{text}[/italic]"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def warning(text: str) -> str:
|
|
65
|
+
return f"[yellow]{text}[/yellow]"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def error(text: str) -> str:
|
|
69
|
+
return f"[red]{text}[/red]"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def code(text: str) -> str:
|
|
73
|
+
return f"[cyan]{text}[/cyan]"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def success(text: str) -> str:
|
|
77
|
+
return f"[green]{text}[/green]"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def link(text: str, link: str) -> str:
|
|
81
|
+
return f"[link={link}]{text}[/link]"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def merge_claude_code_options(base_options: Any, **overrides: Any) -> Any:
|
|
85
|
+
"""
|
|
86
|
+
Create a new ClaudeCodeOptions instance by merging base options with overrides.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
base_options: Base ClaudeCodeOptions instance to copy from
|
|
90
|
+
**overrides: Dictionary of option overrides
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
New ClaudeCodeOptions instance with merged options
|
|
94
|
+
"""
|
|
95
|
+
with patched_typing():
|
|
96
|
+
from claude_code_sdk import ClaudeCodeOptions
|
|
97
|
+
|
|
98
|
+
# Create a new options instance with the base values
|
|
99
|
+
options = ClaudeCodeOptions()
|
|
100
|
+
|
|
101
|
+
# Copy all attributes from base_options
|
|
102
|
+
if base_options:
|
|
103
|
+
for attr in [
|
|
104
|
+
"model",
|
|
105
|
+
"max_thinking_tokens",
|
|
106
|
+
"max_turns",
|
|
107
|
+
"cwd",
|
|
108
|
+
"system_prompt",
|
|
109
|
+
"append_system_prompt",
|
|
110
|
+
"permission_mode",
|
|
111
|
+
"permission_prompt_tool_name",
|
|
112
|
+
"continue_conversation",
|
|
113
|
+
"resume",
|
|
114
|
+
"allowed_tools",
|
|
115
|
+
"disallowed_tools",
|
|
116
|
+
"mcp_servers",
|
|
117
|
+
"mcp_tools",
|
|
118
|
+
# Anthropic API fields
|
|
119
|
+
"temperature",
|
|
120
|
+
"top_p",
|
|
121
|
+
"top_k",
|
|
122
|
+
"stop_sequences",
|
|
123
|
+
"tools",
|
|
124
|
+
"metadata",
|
|
125
|
+
"service_tier",
|
|
126
|
+
]:
|
|
127
|
+
if hasattr(base_options, attr):
|
|
128
|
+
base_value = getattr(base_options, attr)
|
|
129
|
+
if base_value is not None:
|
|
130
|
+
setattr(options, attr, base_value)
|
|
131
|
+
|
|
132
|
+
# Apply overrides
|
|
133
|
+
for key, value in overrides.items():
|
|
134
|
+
if value is not None and hasattr(options, key):
|
|
135
|
+
# Handle special type conversions for specific fields
|
|
136
|
+
if key == "cwd" and not isinstance(value, str):
|
|
137
|
+
value = str(value)
|
|
138
|
+
setattr(options, key, value)
|
|
139
|
+
|
|
140
|
+
return options
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_running_in_docker() -> bool:
|
|
144
|
+
return Path("/.dockerenv").exists()
|