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,630 @@
|
|
|
1
|
+
"""Serve command for CCProxy API server - consolidates server-related commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import uvicorn
|
|
10
|
+
from click import get_current_context
|
|
11
|
+
from structlog import get_logger
|
|
12
|
+
|
|
13
|
+
from ccproxy._version import __version__
|
|
14
|
+
from ccproxy.cli.helpers import (
|
|
15
|
+
get_rich_toolkit,
|
|
16
|
+
is_running_in_docker,
|
|
17
|
+
warning,
|
|
18
|
+
)
|
|
19
|
+
from ccproxy.config.settings import (
|
|
20
|
+
ConfigurationError,
|
|
21
|
+
Settings,
|
|
22
|
+
config_manager,
|
|
23
|
+
)
|
|
24
|
+
from ccproxy.core.async_utils import get_root_package_name
|
|
25
|
+
from ccproxy.docker import (
|
|
26
|
+
DockerEnv,
|
|
27
|
+
DockerPath,
|
|
28
|
+
DockerUserContext,
|
|
29
|
+
DockerVolume,
|
|
30
|
+
create_docker_adapter,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from ..docker import (
|
|
34
|
+
_create_docker_adapter_from_settings,
|
|
35
|
+
)
|
|
36
|
+
from ..docker.params import (
|
|
37
|
+
docker_arg_option,
|
|
38
|
+
docker_env_option,
|
|
39
|
+
docker_home_option,
|
|
40
|
+
docker_image_option,
|
|
41
|
+
docker_volume_option,
|
|
42
|
+
docker_workspace_option,
|
|
43
|
+
user_gid_option,
|
|
44
|
+
user_mapping_option,
|
|
45
|
+
user_uid_option,
|
|
46
|
+
)
|
|
47
|
+
from ..options.claude_options import (
|
|
48
|
+
ClaudeOptions,
|
|
49
|
+
allowed_tools_option,
|
|
50
|
+
append_system_prompt_option,
|
|
51
|
+
claude_cli_path_option,
|
|
52
|
+
cwd_option,
|
|
53
|
+
disallowed_tools_option,
|
|
54
|
+
max_thinking_tokens_option,
|
|
55
|
+
max_turns_option,
|
|
56
|
+
permission_mode_option,
|
|
57
|
+
permission_prompt_tool_name_option,
|
|
58
|
+
)
|
|
59
|
+
from ..options.core_options import CoreOptions, config_option
|
|
60
|
+
from ..options.security_options import SecurityOptions, auth_token_option
|
|
61
|
+
from ..options.server_options import (
|
|
62
|
+
ServerOptions,
|
|
63
|
+
host_option,
|
|
64
|
+
log_file_option,
|
|
65
|
+
log_level_option,
|
|
66
|
+
port_option,
|
|
67
|
+
reload_option,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Logger will be configured by configuration manager
|
|
72
|
+
logger = get_logger(__name__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_config_path_from_context() -> Path | None:
|
|
76
|
+
"""Get config path from typer context if available."""
|
|
77
|
+
try:
|
|
78
|
+
ctx = get_current_context()
|
|
79
|
+
if ctx and ctx.obj and "config_path" in ctx.obj:
|
|
80
|
+
config_path = ctx.obj["config_path"]
|
|
81
|
+
return config_path if config_path is None else Path(config_path)
|
|
82
|
+
except RuntimeError:
|
|
83
|
+
# No active click context (e.g., in tests)
|
|
84
|
+
pass
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _show_api_usage_info(toolkit: Any, settings: Settings) -> None:
|
|
89
|
+
"""Show API usage information when auth token is configured."""
|
|
90
|
+
from rich.console import Console
|
|
91
|
+
from rich.syntax import Syntax
|
|
92
|
+
|
|
93
|
+
toolkit.print_title("API Client Configuration", tag="config")
|
|
94
|
+
|
|
95
|
+
# Determine the base URLs
|
|
96
|
+
anthropic_base_url = f"http://{settings.server.host}:{settings.server.port}"
|
|
97
|
+
openai_base_url = f"http://{settings.server.host}:{settings.server.port}/openai"
|
|
98
|
+
|
|
99
|
+
# Show environment variable exports using code blocks
|
|
100
|
+
toolkit.print("Environment Variables for API Clients:", tag="info")
|
|
101
|
+
toolkit.print_line()
|
|
102
|
+
|
|
103
|
+
# Use rich console for code blocks
|
|
104
|
+
console = Console()
|
|
105
|
+
|
|
106
|
+
exports = f"""export ANTHROPIC_API_KEY={settings.security.auth_token}
|
|
107
|
+
export ANTHROPIC_BASE_URL={anthropic_base_url}
|
|
108
|
+
export OPENAI_API_KEY={settings.security.auth_token}
|
|
109
|
+
export OPENAI_BASE_URL={openai_base_url}"""
|
|
110
|
+
|
|
111
|
+
console.print(Syntax(exports, "bash", theme="monokai", background_color="default"))
|
|
112
|
+
toolkit.print_line()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run_docker_server(
|
|
116
|
+
settings: Settings,
|
|
117
|
+
docker_image: str | None = None,
|
|
118
|
+
docker_env: list[str] | None = None,
|
|
119
|
+
docker_volume: list[str] | None = None,
|
|
120
|
+
docker_arg: list[str] | None = None,
|
|
121
|
+
docker_home: str | None = None,
|
|
122
|
+
docker_workspace: str | None = None,
|
|
123
|
+
user_mapping_enabled: bool | None = None,
|
|
124
|
+
user_uid: int | None = None,
|
|
125
|
+
user_gid: int | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Run the server using Docker."""
|
|
128
|
+
toolkit = get_rich_toolkit()
|
|
129
|
+
logger = get_logger(__name__)
|
|
130
|
+
|
|
131
|
+
docker_env = docker_env or []
|
|
132
|
+
docker_volume = docker_volume or []
|
|
133
|
+
docker_arg = docker_arg or []
|
|
134
|
+
|
|
135
|
+
docker_env_dict = {}
|
|
136
|
+
for env_var in docker_env:
|
|
137
|
+
if "=" in env_var:
|
|
138
|
+
key, value = env_var.split("=", 1)
|
|
139
|
+
docker_env_dict[key] = value
|
|
140
|
+
|
|
141
|
+
# Add server configuration to Docker environment
|
|
142
|
+
if settings.server.reload:
|
|
143
|
+
docker_env_dict["RELOAD"] = "true"
|
|
144
|
+
docker_env_dict["PORT"] = str(settings.server.port)
|
|
145
|
+
docker_env_dict["HOST"] = "0.0.0.0"
|
|
146
|
+
|
|
147
|
+
# Display startup information
|
|
148
|
+
# toolkit.print_title(
|
|
149
|
+
# "Starting CCProxy API server with Docker", tag="docker"
|
|
150
|
+
# )
|
|
151
|
+
# toolkit.print(
|
|
152
|
+
# f"Server will be available at: http://{settings.server.host}:{settings.server.port}",
|
|
153
|
+
# tag="info",
|
|
154
|
+
# )
|
|
155
|
+
toolkit.print_line()
|
|
156
|
+
|
|
157
|
+
# Show Docker configuration summary
|
|
158
|
+
toolkit.print_title("Docker Configuration Summary", tag="config")
|
|
159
|
+
|
|
160
|
+
# Determine effective directories for volume mapping
|
|
161
|
+
home_dir = docker_home or settings.docker.docker_home_directory
|
|
162
|
+
workspace_dir = docker_workspace or settings.docker.docker_workspace_directory
|
|
163
|
+
|
|
164
|
+
# Show volume information
|
|
165
|
+
toolkit.print("Volumes:", tag="config")
|
|
166
|
+
if home_dir:
|
|
167
|
+
toolkit.print(f" Home: {home_dir} → /data/home", tag="volume")
|
|
168
|
+
if workspace_dir:
|
|
169
|
+
toolkit.print(f" Workspace: {workspace_dir} → /data/workspace", tag="volume")
|
|
170
|
+
if docker_volume:
|
|
171
|
+
for vol in docker_volume:
|
|
172
|
+
toolkit.print(f" Additional: {vol}", tag="volume")
|
|
173
|
+
toolkit.print_line()
|
|
174
|
+
|
|
175
|
+
# Show environment information
|
|
176
|
+
toolkit.print("Environment Variables:", tag="config")
|
|
177
|
+
key_env_vars = {
|
|
178
|
+
"CLAUDE_HOME": "/data/home",
|
|
179
|
+
"CLAUDE_WORKSPACE": "/data/workspace",
|
|
180
|
+
"PORT": str(settings.server.port),
|
|
181
|
+
"HOST": "0.0.0.0",
|
|
182
|
+
}
|
|
183
|
+
if settings.server.reload:
|
|
184
|
+
key_env_vars["RELOAD"] = "true"
|
|
185
|
+
|
|
186
|
+
for key, value in key_env_vars.items():
|
|
187
|
+
toolkit.print(f" {key}={value}", tag="env")
|
|
188
|
+
|
|
189
|
+
# Show additional environment variables from CLI
|
|
190
|
+
for env_var in docker_env:
|
|
191
|
+
toolkit.print(f" {env_var}", tag="env")
|
|
192
|
+
|
|
193
|
+
# Show debug environment information if log level is DEBUG
|
|
194
|
+
if settings.server.log_level == "DEBUG":
|
|
195
|
+
toolkit.print_line()
|
|
196
|
+
toolkit.print_title("Debug: All Environment Variables", tag="debug")
|
|
197
|
+
all_env = {**docker_env_dict}
|
|
198
|
+
for key, value in sorted(all_env.items()):
|
|
199
|
+
toolkit.print(f" {key}={value}", tag="debug")
|
|
200
|
+
|
|
201
|
+
toolkit.print_line()
|
|
202
|
+
|
|
203
|
+
toolkit.print_line()
|
|
204
|
+
|
|
205
|
+
# Show API usage information if auth token is configured
|
|
206
|
+
if settings.security.auth_token:
|
|
207
|
+
_show_api_usage_info(toolkit, settings)
|
|
208
|
+
|
|
209
|
+
# Execute using the new Docker adapter
|
|
210
|
+
image, volumes, environment, command, user_context, additional_args = (
|
|
211
|
+
_create_docker_adapter_from_settings(
|
|
212
|
+
settings,
|
|
213
|
+
command=["ccproxy", "serve"],
|
|
214
|
+
docker_image=docker_image,
|
|
215
|
+
docker_env=[f"{k}={v}" for k, v in docker_env_dict.items()],
|
|
216
|
+
docker_volume=docker_volume,
|
|
217
|
+
docker_arg=docker_arg,
|
|
218
|
+
docker_home=docker_home,
|
|
219
|
+
docker_workspace=docker_workspace,
|
|
220
|
+
user_mapping_enabled=user_mapping_enabled,
|
|
221
|
+
user_uid=user_uid,
|
|
222
|
+
user_gid=user_gid,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
logger.info(
|
|
227
|
+
"docker_server_config",
|
|
228
|
+
configured_image=settings.docker.docker_image,
|
|
229
|
+
effective_image=image,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Add port mapping
|
|
233
|
+
ports = [f"{settings.server.port}:{settings.server.port}"]
|
|
234
|
+
|
|
235
|
+
# Create Docker adapter and execute
|
|
236
|
+
adapter = create_docker_adapter()
|
|
237
|
+
adapter.exec_container(
|
|
238
|
+
image=image,
|
|
239
|
+
volumes=volumes,
|
|
240
|
+
environment=environment,
|
|
241
|
+
command=command,
|
|
242
|
+
user_context=user_context,
|
|
243
|
+
ports=ports,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _run_local_server(settings: Settings, cli_overrides: dict[str, Any]) -> None:
|
|
248
|
+
"""Run the server locally."""
|
|
249
|
+
in_docker = is_running_in_docker()
|
|
250
|
+
toolkit = get_rich_toolkit()
|
|
251
|
+
logger = get_logger(__name__)
|
|
252
|
+
|
|
253
|
+
if in_docker:
|
|
254
|
+
toolkit.print_title(
|
|
255
|
+
f"Starting CCProxy API server in {warning('docker')}",
|
|
256
|
+
tag="docker",
|
|
257
|
+
)
|
|
258
|
+
toolkit.print(
|
|
259
|
+
f"uid={warning(str(os.getuid()))} gid={warning(str(os.getgid()))}"
|
|
260
|
+
)
|
|
261
|
+
toolkit.print(f"HOME={os.environ['HOME']}")
|
|
262
|
+
# else:
|
|
263
|
+
# toolkit.print_title("Starting CCProxy API server", tag="local")
|
|
264
|
+
|
|
265
|
+
# toolkit.print(
|
|
266
|
+
# f"Server will be available at: http://{settings.server.host}:{settings.server.port}",
|
|
267
|
+
# tag="info",
|
|
268
|
+
# )
|
|
269
|
+
|
|
270
|
+
# toolkit.print_line()
|
|
271
|
+
|
|
272
|
+
# Show API usage information if auth token is configured
|
|
273
|
+
if settings.security.auth_token:
|
|
274
|
+
_show_api_usage_info(toolkit, settings)
|
|
275
|
+
|
|
276
|
+
# Set environment variables for server to access CLI overrides
|
|
277
|
+
if cli_overrides:
|
|
278
|
+
os.environ["CCPROXY_CONFIG_OVERRIDES"] = json.dumps(cli_overrides)
|
|
279
|
+
|
|
280
|
+
logger.debug(
|
|
281
|
+
"server_starting",
|
|
282
|
+
host=settings.server.host,
|
|
283
|
+
port=settings.server.port,
|
|
284
|
+
url=f"http://{settings.server.host}:{settings.server.port}",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
reload_includes = None
|
|
288
|
+
if settings.server.reload:
|
|
289
|
+
reload_includes = ["ccproxy", "pyproject.toml", "uv.lock"]
|
|
290
|
+
|
|
291
|
+
# Run uvicorn with our already configured logging
|
|
292
|
+
uvicorn.run(
|
|
293
|
+
app=f"{get_root_package_name()}.api.app:create_app",
|
|
294
|
+
factory=True,
|
|
295
|
+
host=settings.server.host,
|
|
296
|
+
port=settings.server.port,
|
|
297
|
+
reload=settings.server.reload,
|
|
298
|
+
workers=None, # ,settings.workers,
|
|
299
|
+
log_config=None,
|
|
300
|
+
access_log=False, # Disable uvicorn's default access logs
|
|
301
|
+
server_header=False, # Disable uvicorn's server header to preserve upstream headers
|
|
302
|
+
reload_includes=reload_includes,
|
|
303
|
+
# log_config=get_uvicorn_log_config(),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def api(
|
|
308
|
+
# Configuration
|
|
309
|
+
config: Path | None = config_option(),
|
|
310
|
+
# Server options
|
|
311
|
+
port: int | None = port_option(),
|
|
312
|
+
host: str | None = host_option(),
|
|
313
|
+
reload: bool | None = reload_option(),
|
|
314
|
+
log_level: str | None = log_level_option(),
|
|
315
|
+
log_file: str | None = log_file_option(),
|
|
316
|
+
# Security options
|
|
317
|
+
auth_token: str | None = auth_token_option(),
|
|
318
|
+
# Claude options
|
|
319
|
+
max_thinking_tokens: int | None = max_thinking_tokens_option(),
|
|
320
|
+
allowed_tools: str | None = allowed_tools_option(),
|
|
321
|
+
disallowed_tools: str | None = disallowed_tools_option(),
|
|
322
|
+
claude_cli_path: str | None = claude_cli_path_option(),
|
|
323
|
+
append_system_prompt: str | None = append_system_prompt_option(),
|
|
324
|
+
permission_mode: str | None = permission_mode_option(),
|
|
325
|
+
max_turns: int | None = max_turns_option(),
|
|
326
|
+
cwd: str | None = cwd_option(),
|
|
327
|
+
permission_prompt_tool_name: str | None = permission_prompt_tool_name_option(),
|
|
328
|
+
# Core settings
|
|
329
|
+
docker: bool = typer.Option(
|
|
330
|
+
False,
|
|
331
|
+
"--docker",
|
|
332
|
+
"-d",
|
|
333
|
+
help="Run API server using Docker instead of local execution",
|
|
334
|
+
),
|
|
335
|
+
# Docker settings using shared parameters
|
|
336
|
+
docker_image: str | None = docker_image_option(),
|
|
337
|
+
docker_env: list[str] = docker_env_option(),
|
|
338
|
+
docker_volume: list[str] = docker_volume_option(),
|
|
339
|
+
docker_arg: list[str] = docker_arg_option(),
|
|
340
|
+
docker_home: str | None = docker_home_option(),
|
|
341
|
+
docker_workspace: str | None = docker_workspace_option(),
|
|
342
|
+
user_mapping_enabled: bool | None = user_mapping_option(),
|
|
343
|
+
user_uid: int | None = user_uid_option(),
|
|
344
|
+
user_gid: int | None = user_gid_option(),
|
|
345
|
+
) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Start the CCProxy API server.
|
|
348
|
+
|
|
349
|
+
This command starts the API server either locally or in Docker.
|
|
350
|
+
The server provides both Anthropic and OpenAI-compatible endpoints.
|
|
351
|
+
|
|
352
|
+
All configuration options can be provided via CLI parameters,
|
|
353
|
+
which override values from configuration files and environment variables.
|
|
354
|
+
|
|
355
|
+
Examples:
|
|
356
|
+
ccproxy serve
|
|
357
|
+
ccproxy serve --port 8080 --reload
|
|
358
|
+
ccproxy serve --docker
|
|
359
|
+
ccproxy serve --docker --docker-image custom:latest --port 8080
|
|
360
|
+
ccproxy serve --max-thinking-tokens 10000 --allowed-tools Read,Write,Bash
|
|
361
|
+
ccproxy serve --port 8080 --workers 4
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
# Early logging - use basic print until logging is configured
|
|
365
|
+
# We'll log this properly after logging is configured
|
|
366
|
+
|
|
367
|
+
# Get config path from context if not provided directly
|
|
368
|
+
if config is None:
|
|
369
|
+
config = get_config_path_from_context()
|
|
370
|
+
|
|
371
|
+
# Create option containers for better organization
|
|
372
|
+
server_options = ServerOptions(
|
|
373
|
+
port=port,
|
|
374
|
+
host=host,
|
|
375
|
+
reload=reload,
|
|
376
|
+
log_level=log_level,
|
|
377
|
+
log_file=log_file,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
claude_options = ClaudeOptions(
|
|
381
|
+
max_thinking_tokens=max_thinking_tokens,
|
|
382
|
+
allowed_tools=allowed_tools,
|
|
383
|
+
disallowed_tools=disallowed_tools,
|
|
384
|
+
claude_cli_path=claude_cli_path,
|
|
385
|
+
append_system_prompt=append_system_prompt,
|
|
386
|
+
permission_mode=permission_mode,
|
|
387
|
+
max_turns=max_turns,
|
|
388
|
+
cwd=cwd,
|
|
389
|
+
permission_prompt_tool_name=permission_prompt_tool_name,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
security_options = SecurityOptions(auth_token=auth_token)
|
|
393
|
+
|
|
394
|
+
# Extract CLI overrides from structured option containers
|
|
395
|
+
cli_overrides = config_manager.get_cli_overrides_from_args(
|
|
396
|
+
# Server options
|
|
397
|
+
host=server_options.host,
|
|
398
|
+
port=server_options.port,
|
|
399
|
+
reload=server_options.reload,
|
|
400
|
+
log_level=server_options.log_level,
|
|
401
|
+
log_file=server_options.log_file,
|
|
402
|
+
# Security options
|
|
403
|
+
auth_token=security_options.auth_token,
|
|
404
|
+
# Claude options
|
|
405
|
+
claude_cli_path=claude_options.claude_cli_path,
|
|
406
|
+
max_thinking_tokens=claude_options.max_thinking_tokens,
|
|
407
|
+
allowed_tools=claude_options.allowed_tools,
|
|
408
|
+
disallowed_tools=claude_options.disallowed_tools,
|
|
409
|
+
append_system_prompt=claude_options.append_system_prompt,
|
|
410
|
+
permission_mode=claude_options.permission_mode,
|
|
411
|
+
max_turns=claude_options.max_turns,
|
|
412
|
+
permission_prompt_tool_name=claude_options.permission_prompt_tool_name,
|
|
413
|
+
cwd=claude_options.cwd,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Load settings with CLI overrides
|
|
417
|
+
settings = config_manager.load_settings(
|
|
418
|
+
config_path=config, cli_overrides=cli_overrides
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Set up logging once with the effective log level
|
|
422
|
+
# Import here to avoid circular import
|
|
423
|
+
import structlog
|
|
424
|
+
|
|
425
|
+
from ccproxy.core.logging import setup_logging
|
|
426
|
+
|
|
427
|
+
# Always reconfigure logging to ensure log level changes are picked up
|
|
428
|
+
# Use JSON logs if explicitly requested via env var
|
|
429
|
+
json_logs = os.environ.get("CCPROXY_JSON_LOGS", "").lower() == "true"
|
|
430
|
+
setup_logging(
|
|
431
|
+
json_logs=json_logs,
|
|
432
|
+
log_level=server_options.log_level or settings.server.log_level,
|
|
433
|
+
log_file=server_options.log_file or settings.server.log_file,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Re-get logger after logging is configured
|
|
437
|
+
logger = get_logger(__name__)
|
|
438
|
+
|
|
439
|
+
# Test debug logging
|
|
440
|
+
logger.debug(
|
|
441
|
+
"Debug logging is enabled",
|
|
442
|
+
effective_log_level=server_options.log_level or settings.server.log_level,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Log CLI command that was deferred
|
|
446
|
+
logger.info(
|
|
447
|
+
"cli_command_starting",
|
|
448
|
+
command="serve",
|
|
449
|
+
docker=docker,
|
|
450
|
+
port=server_options.port,
|
|
451
|
+
host=server_options.host,
|
|
452
|
+
config_path=str(config) if config else None,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Log effective configuration
|
|
456
|
+
logger.info(
|
|
457
|
+
"configuration_loaded",
|
|
458
|
+
host=settings.server.host,
|
|
459
|
+
port=settings.server.port,
|
|
460
|
+
log_level=settings.server.log_level,
|
|
461
|
+
log_file=settings.server.log_file,
|
|
462
|
+
docker_mode=docker,
|
|
463
|
+
docker_image=settings.docker.docker_image if docker else None,
|
|
464
|
+
auth_enabled=bool(settings.security.auth_token),
|
|
465
|
+
duckdb_enabled=settings.observability.duckdb_enabled,
|
|
466
|
+
duckdb_path=settings.observability.duckdb_path
|
|
467
|
+
if settings.observability.duckdb_enabled
|
|
468
|
+
else None,
|
|
469
|
+
claude_cli_path=settings.claude.cli_path,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if docker:
|
|
473
|
+
_run_docker_server(
|
|
474
|
+
settings,
|
|
475
|
+
docker_image=docker_image,
|
|
476
|
+
docker_env=docker_env,
|
|
477
|
+
docker_volume=docker_volume,
|
|
478
|
+
docker_arg=docker_arg,
|
|
479
|
+
docker_home=docker_home,
|
|
480
|
+
docker_workspace=docker_workspace,
|
|
481
|
+
user_mapping_enabled=user_mapping_enabled,
|
|
482
|
+
user_uid=user_uid,
|
|
483
|
+
user_gid=user_gid,
|
|
484
|
+
)
|
|
485
|
+
else:
|
|
486
|
+
_run_local_server(settings, cli_overrides)
|
|
487
|
+
|
|
488
|
+
except ConfigurationError as e:
|
|
489
|
+
toolkit = get_rich_toolkit()
|
|
490
|
+
toolkit.print(f"Configuration error: {e}", tag="error")
|
|
491
|
+
raise typer.Exit(1) from e
|
|
492
|
+
except Exception as e:
|
|
493
|
+
toolkit = get_rich_toolkit()
|
|
494
|
+
toolkit.print(f"Error starting server: {e}", tag="error")
|
|
495
|
+
raise typer.Exit(1) from e
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def claude(
|
|
499
|
+
args: list[str] | None = typer.Argument(
|
|
500
|
+
default=None,
|
|
501
|
+
help="Arguments to pass to claude CLI (e.g. --version, doctor, config)",
|
|
502
|
+
),
|
|
503
|
+
docker: bool = typer.Option(
|
|
504
|
+
False,
|
|
505
|
+
"--docker",
|
|
506
|
+
"-d",
|
|
507
|
+
help="Run claude command from docker image instead of local CLI",
|
|
508
|
+
),
|
|
509
|
+
# Docker settings using shared parameters
|
|
510
|
+
docker_image: str | None = docker_image_option(),
|
|
511
|
+
docker_env: list[str] = docker_env_option(),
|
|
512
|
+
docker_volume: list[str] = docker_volume_option(),
|
|
513
|
+
docker_arg: list[str] = docker_arg_option(),
|
|
514
|
+
docker_home: str | None = docker_home_option(),
|
|
515
|
+
docker_workspace: str | None = docker_workspace_option(),
|
|
516
|
+
user_mapping_enabled: bool | None = user_mapping_option(),
|
|
517
|
+
user_uid: int | None = user_uid_option(),
|
|
518
|
+
user_gid: int | None = user_gid_option(),
|
|
519
|
+
) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Execute claude CLI commands directly.
|
|
522
|
+
|
|
523
|
+
This is a simple pass-through to the claude CLI executable
|
|
524
|
+
found by the settings system or run from docker image.
|
|
525
|
+
|
|
526
|
+
Examples:
|
|
527
|
+
ccproxy claude -- --version
|
|
528
|
+
ccproxy claude -- doctor
|
|
529
|
+
ccproxy claude -- config
|
|
530
|
+
ccproxy claude --docker -- --version
|
|
531
|
+
ccproxy claude --docker --docker-image custom:latest -- --version
|
|
532
|
+
ccproxy claude --docker --docker-env API_KEY=sk-... --docker-volume ./data:/data -- chat
|
|
533
|
+
"""
|
|
534
|
+
# Handle None args case
|
|
535
|
+
if args is None:
|
|
536
|
+
args = []
|
|
537
|
+
|
|
538
|
+
toolkit = get_rich_toolkit()
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
# Log CLI command execution start
|
|
542
|
+
logger.info(
|
|
543
|
+
"cli_command_starting",
|
|
544
|
+
command="claude",
|
|
545
|
+
docker=docker,
|
|
546
|
+
args=args if args else [],
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Load settings using configuration manager
|
|
550
|
+
settings = config_manager.load_settings(
|
|
551
|
+
config_path=get_config_path_from_context()
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if docker:
|
|
555
|
+
# Prepare Docker execution using new adapter
|
|
556
|
+
|
|
557
|
+
toolkit.print_title(f"image {settings.docker.docker_image}", tag="docker")
|
|
558
|
+
image, volumes, environment, command, user_context, additional_args = (
|
|
559
|
+
_create_docker_adapter_from_settings(
|
|
560
|
+
settings,
|
|
561
|
+
docker_image=docker_image,
|
|
562
|
+
docker_env=docker_env,
|
|
563
|
+
docker_volume=docker_volume,
|
|
564
|
+
docker_arg=docker_arg,
|
|
565
|
+
docker_home=docker_home,
|
|
566
|
+
docker_workspace=docker_workspace,
|
|
567
|
+
user_mapping_enabled=user_mapping_enabled,
|
|
568
|
+
user_uid=user_uid,
|
|
569
|
+
user_gid=user_gid,
|
|
570
|
+
command=["claude"],
|
|
571
|
+
cmd_args=args,
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
cmd_str = " ".join(command or [])
|
|
576
|
+
logger.info(
|
|
577
|
+
"docker_execution",
|
|
578
|
+
image=image,
|
|
579
|
+
command=" ".join(command or []),
|
|
580
|
+
volumes_count=len(volumes),
|
|
581
|
+
env_vars_count=len(environment),
|
|
582
|
+
)
|
|
583
|
+
toolkit.print(f"Executing: docker run ... {image} {cmd_str}", tag="docker")
|
|
584
|
+
toolkit.print_line()
|
|
585
|
+
|
|
586
|
+
# Execute using the new Docker adapter
|
|
587
|
+
adapter = create_docker_adapter()
|
|
588
|
+
adapter.exec_container(
|
|
589
|
+
image=image,
|
|
590
|
+
volumes=volumes,
|
|
591
|
+
environment=environment,
|
|
592
|
+
command=command,
|
|
593
|
+
user_context=user_context,
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
# Get claude path from settings
|
|
597
|
+
claude_path = settings.claude.cli_path
|
|
598
|
+
if not claude_path:
|
|
599
|
+
toolkit.print("Error: Claude CLI not found.", tag="error")
|
|
600
|
+
toolkit.print(
|
|
601
|
+
"Please install Claude CLI or configure claude_cli_path.",
|
|
602
|
+
tag="error",
|
|
603
|
+
)
|
|
604
|
+
raise typer.Exit(1)
|
|
605
|
+
|
|
606
|
+
# Resolve to absolute path
|
|
607
|
+
if not Path(claude_path).is_absolute():
|
|
608
|
+
claude_path = str(Path(claude_path).resolve())
|
|
609
|
+
|
|
610
|
+
logger.info("local_claude_execution", claude_path=claude_path, args=args)
|
|
611
|
+
toolkit.print(f"Executing: {claude_path} {' '.join(args)}", tag="claude")
|
|
612
|
+
toolkit.print_line()
|
|
613
|
+
|
|
614
|
+
# Execute command directly
|
|
615
|
+
try:
|
|
616
|
+
# Use os.execvp to replace current process with claude
|
|
617
|
+
# This hands over full control to claude, including signal handling
|
|
618
|
+
os.execvp(claude_path, [claude_path] + args)
|
|
619
|
+
except OSError as e:
|
|
620
|
+
toolkit.print(f"Failed to execute command: {e}", tag="error")
|
|
621
|
+
raise typer.Exit(1) from e
|
|
622
|
+
|
|
623
|
+
except ConfigurationError as e:
|
|
624
|
+
logger.error("cli_configuration_error", error=str(e), command="claude")
|
|
625
|
+
toolkit.print(f"Configuration error: {e}", tag="error")
|
|
626
|
+
raise typer.Exit(1) from e
|
|
627
|
+
except Exception as e:
|
|
628
|
+
logger.error("cli_unexpected_error", error=str(e), command="claude")
|
|
629
|
+
toolkit.print(f"Error executing claude command: {e}", tag="error")
|
|
630
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Docker-related CLI utilities for Claude Code Proxy."""
|
|
2
|
+
|
|
3
|
+
from ccproxy.cli.docker.adapter_factory import (
|
|
4
|
+
_create_docker_adapter_from_settings,
|
|
5
|
+
)
|
|
6
|
+
from ccproxy.cli.docker.params import (
|
|
7
|
+
DockerOptions,
|
|
8
|
+
docker_arg_option,
|
|
9
|
+
docker_env_option,
|
|
10
|
+
docker_home_option,
|
|
11
|
+
docker_image_option,
|
|
12
|
+
docker_volume_option,
|
|
13
|
+
docker_workspace_option,
|
|
14
|
+
user_gid_option,
|
|
15
|
+
user_mapping_option,
|
|
16
|
+
user_uid_option,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Factory functions
|
|
22
|
+
"_create_docker_adapter_from_settings",
|
|
23
|
+
# Docker options
|
|
24
|
+
"DockerOptions",
|
|
25
|
+
"docker_image_option",
|
|
26
|
+
"docker_env_option",
|
|
27
|
+
"docker_volume_option",
|
|
28
|
+
"docker_arg_option",
|
|
29
|
+
"docker_home_option",
|
|
30
|
+
"docker_workspace_option",
|
|
31
|
+
"user_mapping_option",
|
|
32
|
+
"user_uid_option",
|
|
33
|
+
"user_gid_option",
|
|
34
|
+
]
|