Skip to content

ccproxy.utils.startup_helpers

ccproxy.utils.startup_helpers

Startup utility functions for application lifecycle management.

This module contains simple utility functions to extract and organize the complex startup logic from the main lifespan function, following the KISS principle and avoiding overengineering.

validate_claude_authentication_startup async

validate_claude_authentication_startup(app, settings)

Validate Claude authentication credentials at startup.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def validate_claude_authentication_startup(
    app: FastAPI, settings: Settings
) -> None:
    """Validate Claude authentication credentials at startup.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        credentials_manager = CredentialsManager()
        validation = await credentials_manager.validate()

        if validation.valid and not validation.expired:
            credentials = validation.credentials
            oauth_token = credentials.claude_ai_oauth if credentials else None

            if oauth_token and oauth_token.expires_at_datetime:
                hours_until_expiry = int(
                    (
                        oauth_token.expires_at_datetime - datetime.now(UTC)
                    ).total_seconds()
                    / 3600
                )
                logger.debug(
                    "claude_token_valid",
                    expires_in_hours=hours_until_expiry,
                    subscription_type=oauth_token.subscription_type,
                    credentials_path=str(validation.path) if validation.path else None,
                )
            else:
                logger.debug(
                    "claude_token_valid", credentials_path=str(validation.path)
                )
        elif validation.expired:
            logger.warning(
                "claude_token_expired",
                message="Claude authentication token has expired. Please run 'ccproxy auth login' to refresh.",
                credentials_path=str(validation.path) if validation.path else None,
            )
        else:
            logger.warning(
                "claude_token_invalid",
                message="Claude authentication token is invalid. Please run 'ccproxy auth login'.",
                credentials_path=str(validation.path) if validation.path else None,
            )
    except CredentialsNotFoundError:
        logger.warning(
            "claude_token_not_found",
            message="No Claude authentication credentials found. Please run 'ccproxy auth login' to authenticate.",
            searched_paths=settings.auth.storage.storage_paths,
        )
    except Exception as e:
        logger.error(
            "claude_token_validation_error",
            error=str(e),
            message="Failed to validate Claude authentication token. The server will continue without Claude authentication.",
            exc_info=True,
        )

validate_codex_authentication_startup async

validate_codex_authentication_startup(app, settings)

Validate Codex (OpenAI) authentication credentials at startup.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def validate_codex_authentication_startup(
    app: FastAPI, settings: Settings
) -> None:
    """Validate Codex (OpenAI) authentication credentials at startup.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    # Skip codex authentication validation if codex is disabled
    if not settings.codex.enabled:
        logger.debug("codex_token_validation_skipped", reason="codex_disabled")
        return

    try:
        token_manager = OpenAITokenManager()
        credentials = await token_manager.load_credentials()

        if not credentials:
            logger.warning(
                "codex_token_not_found",
                message="No Codex authentication credentials found. Please run 'ccproxy auth login-openai' to authenticate.",
                location=token_manager.get_storage_location(),
            )
            return

        if not credentials.active:
            logger.warning(
                "codex_token_inactive",
                message="Codex authentication credentials are inactive. Please run 'ccproxy auth login-openai' to refresh.",
                location=token_manager.get_storage_location(),
            )
            return

        if credentials.is_expired():
            logger.warning(
                "codex_token_expired",
                message="Codex authentication token has expired. Please run 'ccproxy auth login-openai' to refresh.",
                location=token_manager.get_storage_location(),
                expires_at=credentials.expires_at.isoformat(),
            )
        else:
            hours_until_expiry = int(credentials.expires_in_seconds() / 3600)
            logger.debug(
                "codex_token_valid",
                expires_in_hours=hours_until_expiry,
                account_id=credentials.account_id,
                location=token_manager.get_storage_location(),
            )

    except Exception as e:
        logger.error(
            "codex_token_validation_error",
            error=str(e),
            message="Failed to validate Codex authentication token. The server will continue without Codex authentication.",
            exc_info=True,
        )

check_version_updates_startup async

check_version_updates_startup(app, settings)

Trigger version update check at startup.

Manually runs the version check task once during application startup, before the scheduler starts managing periodic checks.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def check_version_updates_startup(app: FastAPI, settings: Settings) -> None:
    """Trigger version update check at startup.

    Manually runs the version check task once during application startup,
    before the scheduler starts managing periodic checks.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    # Skip version check if disabled by settings
    if not settings.scheduler.version_check_enabled:
        logger.debug("version_check_startup_disabled")
        return

    try:
        # Import locally to avoid circular imports and create task instance
        from ccproxy.scheduler.tasks import VersionUpdateCheckTask

        # Create a temporary task instance for startup check
        version_task = VersionUpdateCheckTask(
            name="version_check_startup",
            interval_seconds=settings.scheduler.version_check_interval_hours * 3600,
            enabled=True,
            version_check_cache_ttl_hours=settings.scheduler.version_check_cache_ttl_hours,
            skip_first_scheduled_run=False,
        )

        # Run the version check once and wait for it to complete
        success = await version_task.run()

        if success:
            logger.debug("version_check_startup_completed")
        else:
            logger.debug("version_check_startup_failed")

    except Exception as e:
        logger.debug(
            "version_check_startup_error",
            error=str(e),
            error_type=type(e).__name__,
        )

check_claude_cli_startup async

check_claude_cli_startup(app, settings)

Check Claude CLI availability at startup.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def check_claude_cli_startup(app: FastAPI, settings: Settings) -> None:
    """Check Claude CLI availability at startup.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        from ccproxy.api.routes.health import get_claude_cli_info

        claude_info = await get_claude_cli_info()

        if claude_info.status == "available":
            logger.info(
                "claude_cli_available",
                status=claude_info.status,
                version=claude_info.version,
                binary_path=claude_info.binary_path,
            )
        else:
            logger.warning(
                "claude_cli_unavailable",
                status=claude_info.status,
                error=claude_info.error,
                binary_path=claude_info.binary_path,
                message=f"Claude CLI status: {claude_info.status}",
            )
    except Exception as e:
        logger.error(
            "claude_cli_check_failed",
            error=str(e),
            message="Failed to check Claude CLI status during startup",
        )

check_codex_cli_startup async

check_codex_cli_startup(app, settings)

Check Codex CLI availability at startup.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def check_codex_cli_startup(app: FastAPI, settings: Settings) -> None:
    """Check Codex CLI availability at startup.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        from ccproxy.api.routes.health import get_codex_cli_info

        codex_info = await get_codex_cli_info()

        if codex_info.status == "available":
            logger.info(
                "codex_cli_available",
                status=codex_info.status,
                version=codex_info.version,
                binary_path=codex_info.binary_path,
            )
        else:
            logger.warning(
                "codex_cli_unavailable",
                status=codex_info.status,
                error=codex_info.error,
                binary_path=codex_info.binary_path,
                message=f"Codex CLI status: {codex_info.status}",
            )
    except Exception as e:
        logger.error(
            "codex_cli_check_failed",
            error=str(e),
            message="Failed to check Codex CLI status during startup",
        )

initialize_log_storage_startup async

initialize_log_storage_startup(app, settings)

Initialize log storage if needed and backend is DuckDB.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_log_storage_startup(app: FastAPI, settings: Settings) -> None:
    """Initialize log storage if needed and backend is DuckDB.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    if (
        settings.observability.needs_storage_backend
        and settings.observability.log_storage_backend == "duckdb"
    ):
        try:
            storage = SimpleDuckDBStorage(
                database_path=settings.observability.duckdb_path
            )
            await storage.initialize()
            app.state.log_storage = storage
            logger.debug(
                "log_storage_initialized",
                backend="duckdb",
                path=str(settings.observability.duckdb_path),
                collection_enabled=settings.observability.logs_collection_enabled,
            )
        except Exception as e:
            logger.error("log_storage_initialization_failed", error=str(e))

initialize_log_storage_shutdown async

initialize_log_storage_shutdown(app)

Close log storage if initialized.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_log_storage_shutdown(app: FastAPI) -> None:
    """Close log storage if initialized.

    Args:
        app: FastAPI application instance
    """
    if hasattr(app.state, "log_storage") and app.state.log_storage:
        try:
            await app.state.log_storage.close()
            logger.debug("log_storage_closed")
        except Exception as e:
            logger.error("log_storage_close_failed", error=str(e))

setup_scheduler_startup async

setup_scheduler_startup(app, settings)

Start scheduler system and configure tasks.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def setup_scheduler_startup(app: FastAPI, settings: Settings) -> None:
    """Start scheduler system and configure tasks.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        scheduler = await start_scheduler(settings)
        app.state.scheduler = scheduler
        logger.debug("scheduler_initialized")

        # Add session pool stats task if session manager is available
        if (
            scheduler
            and hasattr(app.state, "session_manager")
            and app.state.session_manager
        ):
            try:
                # Add session pool stats task that runs every minute
                await scheduler.add_task(
                    task_name="session_pool_stats",
                    task_type="pool_stats",
                    interval_seconds=60,  # Every minute
                    enabled=True,
                    pool_manager=app.state.session_manager,
                )
                logger.debug("session_pool_stats_task_added", interval_seconds=60)
            except Exception as e:
                logger.error(
                    "session_pool_stats_task_add_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                )
    except SchedulerError as e:
        logger.error("scheduler_initialization_failed", error=str(e))

setup_scheduler_shutdown async

setup_scheduler_shutdown(app)

Stop scheduler system.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
Source code in ccproxy/utils/startup_helpers.py
async def setup_scheduler_shutdown(app: FastAPI) -> None:
    """Stop scheduler system.

    Args:
        app: FastAPI application instance
    """
    try:
        scheduler = getattr(app.state, "scheduler", None)
        await stop_scheduler(scheduler)
        logger.debug("scheduler_stopped_lifespan")
    except SchedulerError as e:
        logger.error("scheduler_stop_failed", error=str(e))

setup_session_manager_shutdown async

setup_session_manager_shutdown(app)

Shutdown Claude SDK session manager if it was created.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
Source code in ccproxy/utils/startup_helpers.py
async def setup_session_manager_shutdown(app: FastAPI) -> None:
    """Shutdown Claude SDK session manager if it was created.

    Args:
        app: FastAPI application instance
    """
    if hasattr(app.state, "session_manager") and app.state.session_manager:
        try:
            await app.state.session_manager.shutdown()
            logger.debug("claude_sdk_session_manager_shutdown")
        except Exception as e:
            logger.error("claude_sdk_session_manager_shutdown_failed", error=str(e))

initialize_claude_detection_startup async

initialize_claude_detection_startup(app, settings)

Initialize Claude detection service.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_claude_detection_startup(app: FastAPI, settings: Settings) -> None:
    """Initialize Claude detection service.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        logger.debug("initializing_claude_detection")
        detection_service = ClaudeDetectionService(settings)
        claude_data = await detection_service.initialize_detection()
        app.state.claude_detection_data = claude_data
        app.state.claude_detection_service = detection_service
        logger.debug(
            "claude_detection_completed",
            version=claude_data.claude_version,
            cached_at=claude_data.cached_at.isoformat(),
        )
    except Exception as e:
        logger.error("claude_detection_startup_failed", error=str(e))
        # Continue startup with fallback - detection service will provide fallback data
        detection_service = ClaudeDetectionService(settings)
        app.state.claude_detection_data = detection_service._get_fallback_data()
        app.state.claude_detection_service = detection_service

initialize_codex_detection_startup async

initialize_codex_detection_startup(app, settings)

Initialize Codex detection service.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_codex_detection_startup(app: FastAPI, settings: Settings) -> None:
    """Initialize Codex detection service.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    # Skip codex detection if codex is disabled
    if not settings.codex.enabled:
        logger.debug("codex_detection_skipped", reason="codex_disabled")
        detection_service = CodexDetectionService(settings)
        app.state.codex_detection_data = detection_service._get_fallback_data()
        app.state.codex_detection_service = detection_service
        return

    # Check if Codex CLI is available before attempting header detection
    from ccproxy.api.routes.health import get_codex_cli_info

    codex_info = await get_codex_cli_info()
    if codex_info.status != "available":
        logger.debug(
            "codex_detection_skipped",
            reason="codex_cli_not_available",
            status=codex_info.status,
        )
        detection_service = CodexDetectionService(settings)
        app.state.codex_detection_data = detection_service._get_fallback_data()
        app.state.codex_detection_service = detection_service
        return

    try:
        logger.debug("initializing_codex_detection")
        detection_service = CodexDetectionService(settings)
        codex_data = await detection_service.initialize_detection()
        app.state.codex_detection_data = codex_data
        app.state.codex_detection_service = detection_service
        logger.debug(
            "codex_detection_completed",
            version=codex_data.codex_version,
            cached_at=codex_data.cached_at.isoformat(),
        )
    except Exception as e:
        logger.error("codex_detection_startup_failed", error=str(e))
        # Continue startup with fallback - detection service will provide fallback data
        detection_service = CodexDetectionService(settings)
        app.state.codex_detection_data = detection_service._get_fallback_data()
        app.state.codex_detection_service = detection_service

initialize_claude_sdk_startup async

initialize_claude_sdk_startup(app, settings)

Initialize ClaudeSDKService and store in app state.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_claude_sdk_startup(app: FastAPI, settings: Settings) -> None:
    """Initialize ClaudeSDKService and store in app state.

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    try:
        # Create auth manager with settings
        auth_manager = CredentialsAuthManager()

        # Get global metrics instance
        metrics = get_metrics()

        # Check if session pool should be enabled from settings configuration
        use_session_pool = settings.claude.sdk_session_pool.enabled

        # Initialize session manager if session pool is enabled
        session_manager = None
        if use_session_pool:
            from ccproxy.claude_sdk.manager import SessionManager

            # Create SessionManager with dependency injection
            session_manager = SessionManager(
                settings=settings, metrics_factory=lambda: metrics
            )

            # Start the session manager (initializes session pool if enabled)
            await session_manager.start()

        # Create ClaudeSDKService instance
        claude_service = ClaudeSDKService(
            auth_manager=auth_manager,
            metrics=metrics,
            settings=settings,
            session_manager=session_manager,
        )

        # Store in app state for reuse in dependencies
        app.state.claude_service = claude_service
        app.state.session_manager = (
            session_manager  # Store session_manager for shutdown
        )
        logger.debug("claude_sdk_service_initialized")
    except Exception as e:
        logger.error("claude_sdk_service_initialization_failed", error=str(e))

initialize_permission_service_startup async

initialize_permission_service_startup(app, settings)

Initialize permission service (conditional on builtin_permissions).

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def initialize_permission_service_startup(
    app: FastAPI, settings: Settings
) -> None:
    """Initialize permission service (conditional on builtin_permissions).

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    if settings.claude.builtin_permissions:
        try:
            from ccproxy.api.services.permission_service import get_permission_service

            permission_service = get_permission_service()

            # Only connect terminal handler if not using external handler
            if settings.server.use_terminal_permission_handler:
                # terminal_handler = TerminalPermissionHandler()

                # TODO: Terminal handler should subscribe to events from the service
                # instead of trying to set a handler directly
                # The service uses an event-based architecture, not direct handlers

                # logger.info(
                #     "permission_handler_configured",
                #     handler_type="terminal",
                #     message="Connected terminal handler to permission service",
                # )
                # app.state.terminal_handler = terminal_handler
                pass
            else:
                logger.debug(
                    "permission_handler_configured",
                    handler_type="external_sse",
                    message="Terminal permission handler disabled - use 'ccproxy permission-handler connect' to handle permissions",
                )
                logger.warning(
                    "permission_handler_required",
                    message="Start external handler with: ccproxy permission-handler connect",
                )

            # Start the permission service
            await permission_service.start()

            # Store references in app state
            app.state.permission_service = permission_service

            logger.debug(
                "permission_service_initialized",
                timeout_seconds=permission_service._timeout_seconds,
                terminal_handler_enabled=settings.server.use_terminal_permission_handler,
                builtin_permissions_enabled=True,
            )
        except Exception as e:
            logger.error("permission_service_initialization_failed", error=str(e))
            # Continue without permission service (API will work but without prompts)
    else:
        logger.debug(
            "permission_service_skipped",
            builtin_permissions_enabled=False,
            message="Built-in permission handling disabled - users can configure custom MCP servers and permission tools",
        )

setup_permission_service_shutdown async

setup_permission_service_shutdown(app, settings)

Stop permission service (if it was initialized).

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
settings Settings

Application settings

required
Source code in ccproxy/utils/startup_helpers.py
async def setup_permission_service_shutdown(app: FastAPI, settings: Settings) -> None:
    """Stop permission service (if it was initialized).

    Args:
        app: FastAPI application instance
        settings: Application settings
    """
    if (
        hasattr(app.state, "permission_service")
        and app.state.permission_service
        and settings.claude.builtin_permissions
    ):
        try:
            await app.state.permission_service.stop()
            logger.debug("permission_service_stopped")
        except Exception as e:
            logger.error("permission_service_stop_failed", error=str(e))

flush_streaming_batches_shutdown async

flush_streaming_batches_shutdown(app)

Flush any remaining streaming log batches.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
Source code in ccproxy/utils/startup_helpers.py
async def flush_streaming_batches_shutdown(app: FastAPI) -> None:
    """Flush any remaining streaming log batches.

    Args:
        app: FastAPI application instance
    """
    try:
        from ccproxy.utils.simple_request_logger import flush_all_streaming_batches

        await flush_all_streaming_batches()
        logger.debug("streaming_batches_flushed")
    except Exception as e:
        logger.error("streaming_batches_flush_failed", error=str(e))