Skip to content

ccproxy.config

ccproxy.config

Configuration module for Claude Proxy API Server.

CORSSettings

Bases: BaseModel

CORS-specific configuration settings.

HTTPSettings

Bases: BaseModel

HTTP client configuration settings.

Controls how the core HTTP client handles compression and other HTTP-level settings.

LoggingSettings

Bases: BaseModel

Centralized logging configuration - core app only.

validate_log_level classmethod

validate_log_level(value)

Validate and normalize log level.

Source code in ccproxy/config/core.py
@field_validator("level", mode="before")
@classmethod
def validate_log_level(cls, value: LogLevelName | str) -> LogLevelName:
    """Validate and normalize log level."""
    if isinstance(value, str):
        candidate = value.upper()
    else:
        candidate = value

    if candidate not in LOG_LEVEL_OPTIONS:
        raise ValueError(
            f"Invalid log level: {value}. Must be one of {list(LOG_LEVEL_OPTIONS)}"
        )

    return cast(LogLevelName, candidate)

validate_log_format classmethod

validate_log_format(value)

Validate and normalize log format.

Source code in ccproxy/config/core.py
@field_validator("format", mode="before")
@classmethod
def validate_log_format(cls, value: LogFormatName | str) -> LogFormatName:
    """Validate and normalize log format."""
    if isinstance(value, str):
        candidate = value.lower()
    else:
        candidate = value

    if candidate not in LOG_FORMAT_OPTIONS:
        raise ValueError(
            f"Invalid log format: {value}. Must be one of {list(LOG_FORMAT_OPTIONS)}"
        )

    return cast(LogFormatName, candidate)

ServerSettings

Bases: BaseModel

Server-specific configuration settings.

Settings

Bases: BaseSettings

Configuration settings for the Claude Proxy API Server.

Settings are loaded from environment variables, .env files, and TOML configuration files. Environment variables take precedence over .env file values. TOML configuration files are loaded in the following order: 1. .ccproxy.toml in current directory 2. ccproxy.toml in git repository root 3. config.toml in XDG_CONFIG_HOME/ccproxy/

server_url property

server_url

Get the complete server URL.

LLMSettings

Bases: BaseModel

LLM-specific feature toggles and defaults.

model_dump_safe

model_dump_safe()

Dump model data with sensitive information masked.

Returns:

Name Type Description
dict dict[str, Any]

Configuration with sensitive data masked

Source code in ccproxy/config/settings.py
def model_dump_safe(self) -> dict[str, Any]:
    """
    Dump model data with sensitive information masked.

    Returns:
        dict: Configuration with sensitive data masked
    """
    return self.model_dump(mode="json")

load_toml_config classmethod

load_toml_config(toml_path)

Load configuration from a TOML file.

Source code in ccproxy/config/settings.py
@classmethod
def load_toml_config(cls, toml_path: Path) -> dict[str, Any]:
    """Load configuration from a TOML file."""
    try:
        with toml_path.open("rb") as f:
            return tomllib.load(f)
    except OSError as e:
        raise ValueError(f"Cannot read TOML config file {toml_path}: {e}") from e
    except tomllib.TOMLDecodeError as e:
        raise ValueError(f"Invalid TOML syntax in {toml_path}: {e}") from e

load_config_file classmethod

load_config_file(config_path)

Load configuration from a file based on its extension.

Source code in ccproxy/config/settings.py
@classmethod
def load_config_file(cls, config_path: Path) -> dict[str, Any]:
    """Load configuration from a file based on its extension."""
    suffix = config_path.suffix.lower()

    if suffix in [".toml"]:
        return cls.load_toml_config(config_path)
    else:
        raise ValueError(
            f"Unsupported config file format: {suffix}. "
            "Only TOML (.toml) files are supported."
        )

from_toml classmethod

from_toml(toml_path=None, **kwargs)

Create Settings instance from TOML configuration.

Source code in ccproxy/config/settings.py
@classmethod
def from_toml(cls, toml_path: Path | None = None, **kwargs: Any) -> "Settings":
    """Create Settings instance from TOML configuration."""
    return cls.from_config(config_path=toml_path, **kwargs)

from_config classmethod

from_config(config_path=None, cli_context=None, **kwargs)

Create Settings instance from configuration file with env precedence and safe merging.

Source code in ccproxy/config/settings.py
@classmethod
def from_config(
    cls,
    config_path: Path | str | None = None,
    cli_context: dict[str, Any] | None = None,
    **kwargs: Any,
) -> "Settings":
    """Create Settings instance from configuration file with env precedence and safe merging."""
    logger = get_logger(__name__)

    global _CONFIG_MISSING_LOGGED

    if config_path is None:
        config_path_env = os.environ.get("CONFIG_FILE")
        if config_path_env:
            config_path = Path(config_path_env)

    if isinstance(config_path, str):
        config_path = Path(config_path)

    if config_path is None:
        config_path = find_toml_config_file()

    config_data: dict[str, Any] = {}
    if config_path and config_path.exists():
        config_data = cls.load_config_file(config_path)
        logger.debug(
            "config_file_loaded",
            path=str(config_path),
            category="config",
        )
    elif not _CONFIG_MISSING_LOGGED:
        suggestion = f"ccproxy config init --output-dir {get_ccproxy_config_dir()}"
        log_kwargs: dict[str, Any] = {
            "category": "config",
            "suggested_command": suggestion,
        }
        if config_path is not None:
            log_kwargs["path"] = str(config_path)
        logger.warning("config_file_missing", **log_kwargs)
        _CONFIG_MISSING_LOGGED = True

    cls._validate_deprecated_keys(config_data)

    # Start from env + .env via BaseSettings
    settings = cls()

    # Merge file-based configuration with env-var precedence
    for key, value in config_data.items():
        if not hasattr(settings, key):
            continue

        if key == "plugins" and isinstance(value, dict):
            current_plugins = getattr(settings, key, {})
            merged_plugins = cls._merge_plugins(current_plugins, value)
            setattr(settings, key, merged_plugins)
            continue

        current_attr = getattr(settings, key)

        if isinstance(value, dict) and isinstance(current_attr, BaseModel):
            merged_model = cls._merge_model(current_attr, value, f"{key.upper()}__")
            setattr(settings, key, merged_model)
        else:
            # Only set top-level simple types if there is no top-level env override
            env_key = key.upper()
            if os.getenv(env_key) is None:
                setattr(settings, key, value)

    # Smart default: if no config file exists and enabled_plugins is still None,
    # set a curated default list of core plugins
    if not config_path or not config_path.exists():
        if settings.enabled_plugins is None:
            settings.enabled_plugins = DEFAULT_ENABLED_PLUGINS

    # Apply direct kwargs overrides (highest precedence within process)
    if kwargs:
        cls._apply_overrides(settings, kwargs)

    # Apply CLI context (explicit flags)
    if cli_context:
        # Store raw CLI context for plugin access
        settings.cli_context = cli_context

        # Apply common serve CLI overrides directly to settings
        server_overrides: dict[str, Any] = {}
        if cli_context.get("host") is not None:
            server_overrides["host"] = cli_context["host"]
        if cli_context.get("port") is not None:
            server_overrides["port"] = cli_context["port"]
        if cli_context.get("reload") is not None:
            server_overrides["reload"] = cli_context["reload"]

        logging_overrides: dict[str, Any] = {}
        if cli_context.get("log_level") is not None:
            logging_overrides["level"] = cli_context["log_level"]
        if cli_context.get("log_file") is not None:
            logging_overrides["file"] = cli_context["log_file"]

        security_overrides: dict[str, Any] = {}
        if cli_context.get("auth_token") is not None:
            security_overrides["auth_token"] = cli_context["auth_token"]

        if server_overrides:
            cls._apply_overrides(settings, {"server": server_overrides})
        if logging_overrides:
            cls._apply_overrides(settings, {"logging": logging_overrides})
        if security_overrides:
            cls._apply_overrides(settings, {"security": security_overrides})

        # Apply plugin enable/disable lists if provided
        enabled_plugins = cli_context.get("enabled_plugins")
        disabled_plugins = cli_context.get("disabled_plugins")
        if enabled_plugins is not None:
            settings.enabled_plugins = list(enabled_plugins)
        if disabled_plugins is not None:
            settings.disabled_plugins = list(disabled_plugins)

    return settings

get_cli_context

get_cli_context()

Get CLI context for plugin access.

Source code in ccproxy/config/settings.py
def get_cli_context(self) -> dict[str, Any]:
    """Get CLI context for plugin access."""
    return self.cli_context

ConfigValidationError

Bases: Exception

Configuration validation error.

validate_config_dict

validate_config_dict(config)

Validate configuration dictionary.

Parameters:

Name Type Description Default
config dict[str, Any]

Configuration dictionary to validate

required

Returns:

Type Description
dict[str, Any]

The validated configuration dictionary

Raises:

Type Description
ConfigValidationError

If configuration is invalid

Source code in ccproxy/config/utils.py
def validate_config_dict(config: dict[str, Any]) -> dict[str, Any]:
    """Validate configuration dictionary.

    Args:
        config: Configuration dictionary to validate

    Returns:
        The validated configuration dictionary

    Raises:
        ConfigValidationError: If configuration is invalid
    """
    if not isinstance(config, dict):
        raise ConfigValidationError("Configuration must be a dictionary")

    validated_config: dict[str, Any] = {}

    # Validate specific fields if present
    if "host" in config:
        validated_config["host"] = validate_host(config["host"])

    if "port" in config:
        validated_config["port"] = validate_port(config["port"])

    if "target_url" in config:
        validated_config["target_url"] = validate_url(config["target_url"])

    if "log_level" in config:
        validated_config["log_level"] = validate_log_level(config["log_level"])

    if "cors_origins" in config:
        validated_config["cors_origins"] = validate_cors_origins(config["cors_origins"])

    if "timeout" in config:
        validated_config["timeout"] = validate_timeout(config["timeout"])

    # Copy other fields without validation
    for key, value in config.items():
        if key not in validated_config:
            validated_config[key] = value

    return validated_config

validate_cors_origins

validate_cors_origins(origins)

Validate CORS origins.

Parameters:

Name Type Description Default
origins list[str]

List of origin URLs to validate

required

Returns:

Type Description
list[str]

The validated list of origins

Raises:

Type Description
ConfigValidationError

If any origin is invalid

Source code in ccproxy/config/utils.py
def validate_cors_origins(origins: list[str]) -> list[str]:
    """Validate CORS origins.

    Args:
        origins: List of origin URLs to validate

    Returns:
        The validated list of origins

    Raises:
        ConfigValidationError: If any origin is invalid
    """
    if not isinstance(origins, list):
        raise ConfigValidationError("CORS origins must be a list")

    validated_origins = []
    for origin in origins:
        if origin == "*":
            validated_origins.append(origin)
        else:
            validated_origins.append(validate_url(origin))

    return validated_origins

validate_host

validate_host(host)

Validate host address.

Parameters:

Name Type Description Default
host str

Host address to validate

required

Returns:

Type Description
str

The validated host address

Raises:

Type Description
ConfigValidationError

If host is invalid

Source code in ccproxy/config/utils.py
def validate_host(host: str) -> str:
    """Validate host address.

    Args:
        host: Host address to validate

    Returns:
        The validated host address

    Raises:
        ConfigValidationError: If host is invalid
    """
    if not host:
        raise ConfigValidationError("Host cannot be empty")

    # Allow localhost, IP addresses, and domain names
    if host in ["localhost", "0.0.0.0", "127.0.0.1"]:
        return host

    # Basic IP address validation
    if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", host):
        parts = host.split(".")
        if all(0 <= int(part) <= 255 for part in parts):
            return host
        raise ConfigValidationError(f"Invalid IP address: {host}")

    # Basic domain name validation
    if re.match(r"^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", host):
        return host

    return host  # Allow other formats for flexibility

validate_log_level

validate_log_level(level)

Validate log level.

Parameters:

Name Type Description Default
level str

Log level to validate

required

Returns:

Type Description
str

The validated log level

Raises:

Type Description
ConfigValidationError

If log level is invalid

Source code in ccproxy/config/utils.py
def validate_log_level(level: str) -> str:
    """Validate log level.

    Args:
        level: Log level to validate

    Returns:
        The validated log level

    Raises:
        ConfigValidationError: If log level is invalid
    """
    valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
    level = level.upper()

    if level not in valid_levels:
        raise ConfigValidationError(
            f"Invalid log level: {level}. Must be one of: {valid_levels}"
        )

    return level

validate_path

validate_path(path)

Validate file path.

Parameters:

Name Type Description Default
path str | Path

Path to validate

required

Returns:

Type Description
Path

The validated Path object

Raises:

Type Description
ConfigValidationError

If path is invalid

Source code in ccproxy/config/utils.py
def validate_path(path: str | Path) -> Path:
    """Validate file path.

    Args:
        path: Path to validate

    Returns:
        The validated Path object

    Raises:
        ConfigValidationError: If path is invalid
    """
    if isinstance(path, str):
        path = Path(path)

    if not isinstance(path, Path):
        raise ConfigValidationError(f"Path must be a string or Path object: {path}")

    return path

validate_port

validate_port(port)

Validate port number.

Parameters:

Name Type Description Default
port int | str

Port number to validate

required

Returns:

Type Description
int

The validated port number

Raises:

Type Description
ConfigValidationError

If port is invalid

Source code in ccproxy/config/utils.py
def validate_port(port: int | str) -> int:
    """Validate port number.

    Args:
        port: Port number to validate

    Returns:
        The validated port number

    Raises:
        ConfigValidationError: If port is invalid
    """
    if isinstance(port, str):
        try:
            port = int(port)
        except ValueError as e:
            raise ConfigValidationError(f"Port must be a valid integer: {port}") from e

    if not isinstance(port, int):
        raise ConfigValidationError(f"Port must be an integer: {port}")

    if port < 1 or port > 65535:
        raise ConfigValidationError(f"Port must be between 1 and 65535: {port}")

    return port

validate_timeout

validate_timeout(timeout)

Validate timeout value.

Parameters:

Name Type Description Default
timeout int | float

Timeout value to validate

required

Returns:

Type Description
int | float

The validated timeout value

Raises:

Type Description
ConfigValidationError

If timeout is invalid

Source code in ccproxy/config/utils.py
def validate_timeout(timeout: int | float) -> int | float:
    """Validate timeout value.

    Args:
        timeout: Timeout value to validate

    Returns:
        The validated timeout value

    Raises:
        ConfigValidationError: If timeout is invalid
    """
    if not isinstance(timeout, int | float):
        raise ConfigValidationError(f"Timeout must be a number: {timeout}")

    if timeout <= 0:
        raise ConfigValidationError(f"Timeout must be positive: {timeout}")

    return timeout

validate_url

validate_url(url)

Validate URL format.

Parameters:

Name Type Description Default
url str

URL to validate

required

Returns:

Type Description
str

The validated URL

Raises:

Type Description
ConfigValidationError

If URL is invalid

Source code in ccproxy/config/utils.py
def validate_url(url: str) -> str:
    """Validate URL format.

    Args:
        url: URL to validate

    Returns:
        The validated URL

    Raises:
        ConfigValidationError: If URL is invalid
    """
    if not url:
        raise ConfigValidationError("URL cannot be empty")

    try:
        result = urlparse(url)
        if not result.scheme or not result.netloc:
            raise ConfigValidationError(f"Invalid URL format: {url}")
    except Exception as e:
        raise ConfigValidationError(f"Invalid URL: {url}") from e

    return url