Skip to content

ccproxy.plugins.copilot

ccproxy.plugins.copilot

GitHub Copilot provider plugin for CCProxy.

This plugin provides OAuth authentication with GitHub and API proxying capabilities for GitHub Copilot services, following the established patterns from existing OAuth Claude and Codex plugins.

CopilotPluginFactory

CopilotPluginFactory()

Bases: BaseProviderPluginFactory, AuthProviderPluginFactory

Factory for GitHub Copilot plugin.

Source code in ccproxy/core/plugins/factories.py
def __init__(self) -> None:
    """Initialize factory with manifest built from class attributes."""
    # Validate required class attributes
    self._validate_class_attributes()

    # Validate runtime class is a proper subclass
    # Import locally to avoid circular import during module import
    from .runtime import ProviderPluginRuntime

    if not issubclass(self.runtime_class, ProviderPluginRuntime):
        raise TypeError(
            f"runtime_class {self.runtime_class.__name__} must be a subclass of ProviderPluginRuntime"
        )

    # Build routes from routers list
    routes = []
    for router_spec in self.routers:
        # Handle both router instances and router factory functions
        router_instance = router_spec.router
        if callable(router_spec.router) and not isinstance(
            router_spec.router, APIRouter
        ):
            # Router is a factory function, call it to get the actual router
            router_instance = router_spec.router()

        routes.append(
            RouteSpec(
                router=cast(APIRouter, router_instance),
                prefix=router_spec.prefix,
                tags=router_spec.tags or [],
                dependencies=router_spec.dependencies,
            )
        )

    # Create manifest from class attributes
    manifest = PluginManifest(
        name=self.plugin_name,
        version=self.plugin_version,
        description=self.plugin_description,
        is_provider=True,
        config_class=self.config_class,
        tool_accumulator_class=self.tool_accumulator_class,
        dependencies=self.dependencies.copy(),
        optional_requires=self.optional_requires.copy(),
        routes=routes,
        tasks=self.tasks.copy(),
        format_adapters=self.format_adapters.copy(),
        requires_format_adapters=self.requires_format_adapters.copy(),
        cli_commands=self.cli_commands.copy(),
        cli_arguments=self.cli_arguments.copy(),
    )

    # Format adapter specification validation is deferred to runtime
    # when settings are available via dependency injection

    # Store the manifest and runtime class directly
    # We don't call parent __init__ because ProviderPluginFactory
    # would override our runtime_class with ProviderPluginRuntime
    self.manifest = manifest
    self.runtime_class = self.__class__.runtime_class

create_context

create_context(core_services)

Create context with all plugin components.

Parameters:

Name Type Description Default
core_services Any

Core services container

required

Returns:

Type Description
PluginContext

Plugin context with all components

Source code in ccproxy/plugins/copilot/plugin.py
def create_context(self, core_services: Any) -> PluginContext:
    """Create context with all plugin components.

    Args:
        core_services: Core services container

    Returns:
        Plugin context with all components
    """
    # Start with base context
    context = super().create_context(core_services)

    # Get or create configuration
    config = context.get("config")
    if not isinstance(config, CopilotConfig):
        config = CopilotConfig()
        context["config"] = config

    # Create OAuth provider
    oauth_provider = self.create_oauth_provider(context)
    context["oauth_provider"] = oauth_provider
    # Also set as auth_provider for AuthProviderPluginRuntime compatibility
    context["auth_provider"] = oauth_provider

    # Create detection service
    detection_service = self.create_detection_service(context)
    context["detection_service"] = detection_service

    # Note: adapter creation is handled asynchronously by create_runtime
    # in factories.py, so we don't create it here in the synchronous context creation

    return context

create_runtime

create_runtime()

Create runtime instance.

Source code in ccproxy/plugins/copilot/plugin.py
def create_runtime(self) -> CopilotPluginRuntime:
    """Create runtime instance."""
    return CopilotPluginRuntime(self.manifest)

create_oauth_provider

create_oauth_provider(context=None)

Create OAuth provider instance.

Parameters:

Name Type Description Default
context PluginContext | None

Plugin context containing shared resources

None

Returns:

Type Description
CopilotOAuthProvider

CopilotOAuthProvider instance

Source code in ccproxy/plugins/copilot/plugin.py
def create_oauth_provider(
    self, context: PluginContext | None = None
) -> CopilotOAuthProvider:
    """Create OAuth provider instance.

    Args:
        context: Plugin context containing shared resources

    Returns:
        CopilotOAuthProvider instance
    """
    if context and isinstance(context.get("config"), CopilotConfig):
        cfg = cast(CopilotConfig, context.get("config"))
    else:
        cfg = CopilotConfig()

    config: CopilotConfig = cfg
    http_client = context.get("http_client") if context else None
    hook_manager = context.get("hook_manager") if context else None
    cli_detection_service = (
        context.get("cli_detection_service") if context else None
    )

    return CopilotOAuthProvider(
        config.oauth,
        http_client=http_client,
        hook_manager=hook_manager,
        detection_service=cli_detection_service,
    )

create_detection_service

create_detection_service(context)

Create detection service instance.

Parameters:

Name Type Description Default
context PluginContext

Plugin context

required

Returns:

Type Description
DetectionServiceProtocol

CopilotDetectionService instance

Source code in ccproxy/plugins/copilot/plugin.py
def create_detection_service(
    self, context: PluginContext
) -> DetectionServiceProtocol:
    """Create detection service instance.

    Args:
        context: Plugin context

    Returns:
        CopilotDetectionService instance
    """
    settings = context.get("settings")
    cli_service = context.get("cli_detection_service")

    if not settings or not cli_service:
        raise ValueError("Settings and CLI detection service required")

    service = CopilotDetectionService(settings, cli_service)
    return cast(DetectionServiceProtocol, service)

create_adapter async

create_adapter(context)

Create main adapter instance.

Parameters:

Name Type Description Default
context PluginContext

Plugin context

required

Returns:

Type Description
BaseAdapter

CopilotAdapter instance

Source code in ccproxy/plugins/copilot/plugin.py
async def create_adapter(self, context: PluginContext) -> BaseAdapter:
    """Create main adapter instance.

    Args:
        context: Plugin context

    Returns:
        CopilotAdapter instance
    """
    if not context:
        raise ValueError("Context required for adapter")

    config = context.get("config")
    if not isinstance(config, CopilotConfig):
        config = CopilotConfig()

    # Get required dependencies following BaseHTTPAdapter pattern
    oauth_provider = context.get("oauth_provider")
    detection_service = context.get("detection_service")
    http_pool_manager = context.get("http_pool_manager")
    auth_manager = context.get("credentials_manager")

    # Optional dependencies
    request_tracer = context.get("request_tracer") or NullRequestTracer()
    metrics = context.get("metrics") or NullMetricsCollector()
    streaming_handler = context.get("streaming_handler") or NullStreamingHandler()
    hook_manager = context.get("hook_manager")

    # Get format_registry from service container
    service_container = context.get("service_container")
    format_registry = None
    if service_container:
        format_registry = service_container.get_format_registry()

    # Debug: Log what we actually have in the context
    logger.debug(
        "copilot_adapter_dependencies_debug",
        context_keys=list(context.keys()) if context else [],
        has_auth_manager=bool(auth_manager),
        has_detection_service=bool(detection_service),
        has_http_pool_manager=bool(http_pool_manager),
        has_oauth_provider=bool(oauth_provider),
        has_format_registry=bool(format_registry),
    )

    if not all([detection_service, http_pool_manager, oauth_provider]):
        missing = []
        if not detection_service:
            missing.append("detection_service")
        if not http_pool_manager:
            missing.append("http_pool_manager")
        if not oauth_provider:
            missing.append("oauth_provider")

        raise ValueError(
            f"Required dependencies missing for CopilotAdapter: {missing}"
        )

    if auth_manager is None:
        configured_override = None
        if hasattr(context, "config") and context.config is not None:
            with contextlib.suppress(AttributeError):
                configured_override = getattr(context.config, "auth_manager", None)

        logger.debug(
            "copilot_adapter_missing_auth_manager",
            reason="unresolved_override",
            configured_override=configured_override,
        )

    return CopilotAdapter(
        config=config,
        auth_manager=auth_manager,
        detection_service=detection_service,
        http_pool_manager=http_pool_manager,
        oauth_provider=oauth_provider,
        request_tracer=request_tracer,
        metrics=metrics,
        streaming_handler=streaming_handler,
        hook_manager=hook_manager,
        format_registry=format_registry,
        context=context,
    )

create_auth_provider

create_auth_provider(context=None)

Create OAuth provider instance for AuthProviderPluginFactory interface.

Parameters:

Name Type Description Default
context PluginContext | None

Plugin context containing shared resources

None

Returns:

Type Description
OAuthProviderProtocol

CopilotOAuthProvider instance

Source code in ccproxy/plugins/copilot/plugin.py
def create_auth_provider(
    self, context: PluginContext | None = None
) -> OAuthProviderProtocol:
    """Create OAuth provider instance for AuthProviderPluginFactory interface.

    Args:
        context: Plugin context containing shared resources

    Returns:
        CopilotOAuthProvider instance
    """
    provider = self.create_oauth_provider(context)
    return cast(OAuthProviderProtocol, provider)

CopilotPluginRuntime

CopilotPluginRuntime(manifest)

Bases: ProviderPluginRuntime, AuthProviderPluginRuntime

Runtime for GitHub Copilot plugin.

Source code in ccproxy/plugins/copilot/plugin.py
def __init__(self, manifest: PluginManifest):
    """Initialize runtime."""
    super().__init__(manifest)
    self.config: CopilotConfig | None = None
    self.adapter: CopilotAdapter | None = None
    self.credential_manager: CopilotTokenManager | None = None
    self.oauth_provider: CopilotOAuthProvider | None = None
    self.detection_service: CopilotDetectionService | None = None

cleanup async

cleanup()

Cleanup plugin resources.

Source code in ccproxy/plugins/copilot/plugin.py
async def cleanup(self) -> None:
    """Cleanup plugin resources."""
    errors = []

    # Cleanup adapter
    if self.adapter:
        try:
            await self.adapter.cleanup()
        except Exception as e:
            errors.append(f"Adapter cleanup failed: {e}")
        finally:
            self.adapter = None

    # Cleanup OAuth provider
    if self.oauth_provider:
        try:
            await self.oauth_provider.cleanup()
        except Exception as e:
            errors.append(f"OAuth provider cleanup failed: {e}")
        finally:
            self.oauth_provider = None

    if self.credential_manager:
        try:
            await self.credential_manager.aclose()
        except Exception as e:
            errors.append(f"Token manager cleanup failed: {e}")
        finally:
            self.credential_manager = None

    if errors:
        logger.error(
            "copilot_plugin_cleanup_failed",
            errors=errors,
        )
    else:
        logger.debug("copilot_plugin_cleanup_completed")