def api(
# Configuration
config: Annotated[
Path | None,
typer.Option(
"--config",
"-c",
help="Path to configuration file (TOML, JSON, or YAML)",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
rich_help_panel="Configuration",
),
] = None,
# Server options
port: Annotated[
int | None,
typer.Option(
"--port",
"-p",
help="Port to run the server on",
callback=validate_port,
rich_help_panel="Server Settings",
),
] = None,
host: Annotated[
str | None,
typer.Option(
"--host",
"-h",
help="Host to bind the server to",
rich_help_panel="Server Settings",
),
] = None,
reload: Annotated[
bool | None,
typer.Option(
"--reload/--no-reload",
help="Enable auto-reload for development",
rich_help_panel="Server Settings",
),
] = None,
log_level: Annotated[
str | None,
typer.Option(
"--log-level",
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Use WARNING for minimal output.",
callback=validate_log_level,
rich_help_panel="Server Settings",
),
] = None,
log_file: Annotated[
str | None,
typer.Option(
"--log-file",
help="Path to JSON log file. If specified, logs will be written to this file in JSON format",
rich_help_panel="Server Settings",
),
] = None,
use_terminal_permission_handler: Annotated[
bool,
typer.Option(
"--terminal-permission-handler",
help="Enable terminal permission terminal handler",
rich_help_panel="Server Settings",
),
] = False,
# Security options
auth_token: Annotated[
str | None,
typer.Option(
"--auth-token",
help="Bearer token for API authentication",
callback=validate_auth_token,
rich_help_panel="Security Settings",
),
] = None,
# Claude options
max_thinking_tokens: Annotated[
int | None,
typer.Option(
"--max-thinking-tokens",
help="Maximum thinking tokens for Claude Code",
callback=validate_max_thinking_tokens,
rich_help_panel="Claude Settings",
),
] = None,
allowed_tools: Annotated[
str | None,
typer.Option(
"--allowed-tools",
help="List of allowed tools (comma-separated)",
rich_help_panel="Claude Settings",
),
] = None,
disallowed_tools: Annotated[
str | None,
typer.Option(
"--disallowed-tools",
help="List of disallowed tools (comma-separated)",
rich_help_panel="Claude Settings",
),
] = None,
claude_cli_path: Annotated[
str | None,
typer.Option(
"--claude-cli-path",
help="Path to Claude CLI executable",
callback=validate_claude_cli_path,
rich_help_panel="Claude Settings",
),
] = None,
append_system_prompt: Annotated[
str | None,
typer.Option(
"--append-system-prompt",
help="Additional system prompt to append",
rich_help_panel="Claude Settings",
),
] = None,
permission_mode: Annotated[
str | None,
typer.Option(
"--permission-mode",
help="Permission mode: default, acceptEdits, or bypassPermissions",
callback=validate_permission_mode,
rich_help_panel="Claude Settings",
),
] = None,
max_turns: Annotated[
int | None,
typer.Option(
"--max-turns",
help="Maximum conversation turns",
callback=validate_max_turns,
rich_help_panel="Claude Settings",
),
] = None,
cwd: Annotated[
str | None,
typer.Option(
"--cwd",
help="Working directory path",
callback=validate_cwd,
rich_help_panel="Claude Settings",
),
] = None,
permission_prompt_tool_name: Annotated[
str | None,
typer.Option(
"--permission-prompt-tool-name",
help="Permission prompt tool name",
rich_help_panel="Claude Settings",
),
] = None,
sdk_message_mode: Annotated[
str | None,
typer.Option(
"--sdk-message-mode",
help="SDK message handling mode: forward (direct SDK blocks), ignore (skip blocks), formatted (XML tags with JSON data)",
callback=validate_sdk_message_mode,
rich_help_panel="Claude Settings",
),
] = None,
sdk_pool: Annotated[
bool,
typer.Option(
"--sdk-pool/--no-sdk-pool",
help="Enable/disable general Claude SDK client connection pooling",
rich_help_panel="Claude Settings",
),
] = False,
sdk_pool_size: Annotated[
int | None,
typer.Option(
"--sdk-pool-size",
help="Number of clients to maintain in the general pool (1-20)",
callback=validate_pool_size,
rich_help_panel="Claude Settings",
),
] = None,
sdk_session_pool: Annotated[
bool,
typer.Option(
"--sdk-session-pool/--no-sdk-session-pool",
help="Enable/disable session-aware Claude SDK client pooling",
rich_help_panel="Claude Settings",
),
] = False,
system_prompt_injection_mode: Annotated[
str | None,
typer.Option(
"--system-prompt-injection-mode",
help="System prompt injection mode: minimal (Claude Code ID only), full (all detected system messages)",
callback=validate_system_prompt_injection_mode,
rich_help_panel="Claude Settings",
),
] = None,
builtin_permissions: Annotated[
bool,
typer.Option(
"--builtin-permissions/--no-builtin-permissions",
help="Enable built-in permission handling infrastructure (MCP server and SSE endpoints). When disabled, users can configure custom MCP servers and permission tools.",
rich_help_panel="Claude Settings",
),
] = True,
# Core settings
docker: Annotated[
bool,
typer.Option(
"--docker",
"-d",
help="Run API server using Docker instead of local execution",
),
] = False,
# Docker settings using shared parameters
docker_image: Annotated[
str | None,
typer.Option(
"--docker-image",
help="Docker image to use (overrides configuration)",
rich_help_panel="Docker Settings",
),
] = None,
docker_env: Annotated[
list[str] | None,
typer.Option(
"--docker-env",
"-e",
help="Environment variables to pass to Docker container",
rich_help_panel="Docker Settings",
),
] = None,
docker_volume: Annotated[
list[str] | None,
typer.Option(
"--docker-volume",
"-v",
help="Volume mounts for Docker container",
rich_help_panel="Docker Settings",
),
] = None,
docker_arg: Annotated[
list[str] | None,
typer.Option(
"--docker-arg",
help="Additional arguments to pass to docker run",
rich_help_panel="Docker Settings",
),
] = None,
docker_home: Annotated[
str | None,
typer.Option(
"--docker-home",
help="Override the home directory for Docker",
rich_help_panel="Docker Settings",
),
] = None,
docker_workspace: Annotated[
str | None,
typer.Option(
"--docker-workspace",
help="Override the workspace directory for Docker",
rich_help_panel="Docker Settings",
),
] = None,
user_mapping_enabled: Annotated[
bool | None,
typer.Option(
"--user-mapping/--no-user-mapping",
help="Enable user mapping for Docker",
rich_help_panel="Docker Settings",
),
] = None,
user_uid: Annotated[
int | None,
typer.Option(
"--user-uid",
help="User UID for Docker user mapping",
rich_help_panel="Docker Settings",
),
] = None,
user_gid: Annotated[
int | None,
typer.Option(
"--user-gid",
help="User GID for Docker user mapping",
rich_help_panel="Docker Settings",
),
] = None,
# Network control flags
no_network_calls: Annotated[
bool,
typer.Option(
"--no-network-calls",
help="Disable all network calls (version checks and pricing updates)",
rich_help_panel="Privacy Settings",
),
] = False,
disable_version_check: Annotated[
bool,
typer.Option(
"--disable-version-check",
help="Disable version update checks (prevents calls to GitHub API)",
rich_help_panel="Privacy Settings",
),
] = False,
disable_pricing_updates: Annotated[
bool,
typer.Option(
"--disable-pricing-updates",
help="Disable pricing data updates (prevents downloads from GitHub)",
rich_help_panel="Privacy Settings",
),
] = False,
) -> None:
"""
Start the CCProxy API server.
This command starts the API server either locally or in Docker.
The server provides both Anthropic and OpenAI-compatible endpoints.
All configuration options can be provided via CLI parameters,
which override values from configuration files and environment variables.
Examples:
ccproxy serve
ccproxy serve --port 8080 --reload
ccproxy serve --docker
ccproxy serve --docker --docker-image custom:latest --port 8080
ccproxy serve --max-thinking-tokens 10000 --allowed-tools Read,Write,Bash
ccproxy serve --port 8080 --workers 4
"""
try:
# Early logging - use basic print until logging is configured
# We'll log this properly after logging is configured
# Get config path from context if not provided directly
if config is None:
config = get_config_path_from_context()
# Create option containers for better organization
server_options = ServerOptions(
port=port,
host=host,
reload=reload,
log_level=log_level,
log_file=log_file,
use_terminal_confirmation_handler=use_terminal_permission_handler,
)
claude_options = ClaudeOptions(
max_thinking_tokens=max_thinking_tokens,
allowed_tools=allowed_tools,
disallowed_tools=disallowed_tools,
claude_cli_path=claude_cli_path,
append_system_prompt=append_system_prompt,
permission_mode=permission_mode,
max_turns=max_turns,
cwd=cwd,
permission_prompt_tool_name=permission_prompt_tool_name,
sdk_message_mode=sdk_message_mode,
sdk_pool=sdk_pool,
sdk_pool_size=sdk_pool_size,
sdk_session_pool=sdk_session_pool,
system_prompt_injection_mode=system_prompt_injection_mode,
builtin_permissions=builtin_permissions,
)
security_options = SecurityOptions(auth_token=auth_token)
# Handle network control flags
scheduler_overrides = {}
if no_network_calls:
# Disable both network features
scheduler_overrides["pricing_update_enabled"] = False
scheduler_overrides["version_check_enabled"] = False
else:
# Handle individual flags
if disable_pricing_updates:
scheduler_overrides["pricing_update_enabled"] = False
if disable_version_check:
scheduler_overrides["version_check_enabled"] = False
# Extract CLI overrides from structured option containers
cli_overrides = config_manager.get_cli_overrides_from_args(
# Server options
host=server_options.host,
port=server_options.port,
reload=server_options.reload,
log_level=server_options.log_level,
log_file=server_options.log_file,
use_terminal_confirmation_handler=server_options.use_terminal_confirmation_handler,
# Security options
auth_token=security_options.auth_token,
# Claude options
claude_cli_path=claude_options.claude_cli_path,
max_thinking_tokens=claude_options.max_thinking_tokens,
allowed_tools=claude_options.allowed_tools,
disallowed_tools=claude_options.disallowed_tools,
append_system_prompt=claude_options.append_system_prompt,
permission_mode=claude_options.permission_mode,
max_turns=claude_options.max_turns,
permission_prompt_tool_name=claude_options.permission_prompt_tool_name,
cwd=claude_options.cwd,
sdk_message_mode=claude_options.sdk_message_mode,
sdk_pool=claude_options.sdk_pool,
sdk_pool_size=claude_options.sdk_pool_size,
sdk_session_pool=claude_options.sdk_session_pool,
system_prompt_injection_mode=claude_options.system_prompt_injection_mode,
builtin_permissions=claude_options.builtin_permissions,
)
# Add scheduler overrides if any
if scheduler_overrides:
cli_overrides["scheduler"] = scheduler_overrides
# Load settings with CLI overrides
settings = config_manager.load_settings(
config_path=config, cli_overrides=cli_overrides
)
# Set up logging once with the effective log level
# Import here to avoid circular import
from ccproxy.core.logging import setup_logging
# Always reconfigure logging to ensure log level changes are picked up
# Use JSON logs if explicitly requested via env var
setup_logging(
json_logs=settings.server.log_format == "json",
log_level_name=settings.server.log_level,
log_file=settings.server.log_file,
)
# Re-get logger after logging is configured
logger = get_logger(__name__)
# Test debug logging
logger.debug(
"Debug logging is enabled",
effective_log_level=server_options.log_level or settings.server.log_level,
)
# Log CLI command that was deferred
logger.info(
"cli_command_starting",
command="serve",
version=__version__,
docker=docker,
port=server_options.port,
host=server_options.host,
config_path=str(config) if config else None,
)
# Log effective configuration
logger.debug(
"configuration_loaded",
host=settings.server.host,
port=settings.server.port,
log_level=settings.server.log_level,
log_file=settings.server.log_file,
docker_mode=docker,
docker_image=settings.docker.docker_image if docker else None,
auth_enabled=bool(settings.security.auth_token),
duckdb_enabled=settings.observability.duckdb_enabled,
duckdb_path=settings.observability.duckdb_path
if settings.observability.duckdb_enabled
else None,
claude_cli_path=settings.claude.cli_path,
)
if docker:
_run_docker_server(
settings,
docker_image=docker_image,
docker_env=docker_env,
docker_volume=docker_volume,
docker_arg=docker_arg,
docker_home=docker_home,
docker_workspace=docker_workspace,
user_mapping_enabled=user_mapping_enabled,
user_uid=user_uid,
user_gid=user_gid,
)
else:
_run_local_server(settings, cli_overrides)
except ConfigurationError as e:
toolkit = get_rich_toolkit()
toolkit.print(f"Configuration error: {e}", tag="error")
raise typer.Exit(1) from e
except Exception as e:
toolkit = get_rich_toolkit()
toolkit.print(f"Error starting server: {e}", tag="error")
raise typer.Exit(1) from e