ccproxy-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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
+ ]