cc-switch-tui 0.2.1

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
mod adapter;
mod auth;
mod claude;
mod codex;
pub mod codex_oauth_auth;
mod gemini;
pub mod streaming;
pub mod streaming_responses;
pub mod transform;
pub mod transform_responses;

use crate::app_config::AppType;
use crate::provider::Provider;
use serde::{Deserialize, Serialize};

pub use adapter::ProviderAdapter;
pub use auth::{AuthInfo, AuthStrategy};
#[allow(unused_imports)]
pub use claude::{
    claude_api_format_needs_transform, get_claude_api_format,
    transform_claude_request_for_api_format, ClaudeAdapter,
};
pub use codex::CodexAdapter;
pub use gemini::GeminiAdapter;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderType {
    Claude,
    ClaudeAuth,
    Codex,
    Gemini,
    GeminiCli,
    OpenRouter,
    GitHubCopilot,
    CodexOAuth,
}

impl ProviderType {
    #[allow(dead_code)]
    pub fn needs_transform(&self) -> bool {
        match self {
            ProviderType::GitHubCopilot | ProviderType::CodexOAuth => true,
            ProviderType::OpenRouter => false,
            _ => false,
        }
    }

    #[allow(dead_code)]
    pub fn default_endpoint(&self) -> &'static str {
        match self {
            ProviderType::Claude | ProviderType::ClaudeAuth => "https://api.anthropic.com",
            ProviderType::Codex => "https://api.openai.com",
            ProviderType::Gemini | ProviderType::GeminiCli => {
                "https://generativelanguage.googleapis.com"
            }
            ProviderType::OpenRouter => "https://openrouter.ai/api",
            ProviderType::GitHubCopilot => "https://api.githubcopilot.com",
            ProviderType::CodexOAuth => "https://chatgpt.com/backend-api/codex",
        }
    }

    #[allow(dead_code)]
    pub fn from_app_type_and_config(app_type: &AppType, provider: &Provider) -> Self {
        match app_type {
            AppType::Claude => {
                if let Some(meta) = provider.meta.as_ref() {
                    if meta.provider_type.as_deref() == Some("github_copilot") {
                        return ProviderType::GitHubCopilot;
                    }
                    if meta.provider_type.as_deref() == Some("codex_oauth") {
                        return ProviderType::CodexOAuth;
                    }
                }

                let adapter = ClaudeAdapter::new();
                if let Ok(base_url) = adapter.extract_base_url(provider) {
                    if base_url.contains("githubcopilot.com") {
                        return ProviderType::GitHubCopilot;
                    }
                    if base_url.contains("openrouter.ai") {
                        return ProviderType::OpenRouter;
                    }
                }

                if let Some(auth_mode) = provider
                    .settings_config
                    .get("auth_mode")
                    .and_then(|value| value.as_str())
                {
                    if auth_mode == "bearer_only" {
                        return ProviderType::ClaudeAuth;
                    }
                }

                if let Some(env) = provider.settings_config.get("env") {
                    if let Some(auth_mode) = env.get("AUTH_MODE").and_then(|value| value.as_str()) {
                        if auth_mode == "bearer_only" {
                            return ProviderType::ClaudeAuth;
                        }
                    }
                }

                ProviderType::Claude
            }
            AppType::Codex => ProviderType::Codex,
            AppType::Gemini => {
                let adapter = GeminiAdapter::new();
                if let Some(auth) = adapter.extract_auth(provider) {
                    let key = &auth.api_key;
                    if key.starts_with("ya29.") || key.starts_with('{') {
                        return ProviderType::GeminiCli;
                    }
                }
                ProviderType::Gemini
            }
            AppType::OpenCode | AppType::OpenClaw => ProviderType::Codex,
            AppType::Hermes => ProviderType::Codex,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            ProviderType::Claude => "claude",
            ProviderType::ClaudeAuth => "claude_auth",
            ProviderType::Codex => "codex",
            ProviderType::Gemini => "gemini",
            ProviderType::GeminiCli => "gemini_cli",
            ProviderType::OpenRouter => "openrouter",
            ProviderType::GitHubCopilot => "github_copilot",
            ProviderType::CodexOAuth => "codex_oauth",
        }
    }
}

impl std::fmt::Display for ProviderType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl std::str::FromStr for ProviderType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "claude" => Ok(ProviderType::Claude),
            "claude_auth" | "claude-auth" => Ok(ProviderType::ClaudeAuth),
            "codex" => Ok(ProviderType::Codex),
            "gemini" => Ok(ProviderType::Gemini),
            "gemini_cli" | "gemini-cli" => Ok(ProviderType::GeminiCli),
            "openrouter" => Ok(ProviderType::OpenRouter),
            "github_copilot" | "github-copilot" | "githubcopilot" => {
                Ok(ProviderType::GitHubCopilot)
            }
            "codex_oauth" | "codex-oauth" | "codexoauth" => Ok(ProviderType::CodexOAuth),
            _ => Err(format!("Invalid provider type: {s}")),
        }
    }
}

pub fn get_adapter(app_type: &AppType) -> Box<dyn ProviderAdapter> {
    match app_type {
        AppType::Claude => Box::new(ClaudeAdapter::new()),
        AppType::Codex => Box::new(CodexAdapter::new()),
        AppType::Gemini => Box::new(GeminiAdapter::new()),
        AppType::OpenCode => Box::new(CodexAdapter::new()),
        AppType::OpenClaw => Box::new(CodexAdapter::new()),
        AppType::Hermes => Box::new(CodexAdapter::new()),
    }
}

#[allow(dead_code)]
pub fn get_adapter_for_provider_type(provider_type: &ProviderType) -> Box<dyn ProviderAdapter> {
    match provider_type {
        ProviderType::Claude
        | ProviderType::ClaudeAuth
        | ProviderType::OpenRouter
        | ProviderType::GitHubCopilot
        | ProviderType::CodexOAuth => Box::new(ClaudeAdapter::new()),
        ProviderType::Codex => Box::new(CodexAdapter::new()),
        ProviderType::Gemini | ProviderType::GeminiCli => Box::new(GeminiAdapter::new()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn create_provider(config: serde_json::Value) -> Provider {
        Provider {
            id: "test".to_string(),
            name: "Test Provider".to_string(),
            settings_config: config,
            website_url: None,
            category: None,
            created_at: None,
            sort_index: None,
            notes: None,
            meta: None,
            icon: None,
            icon_color: None,
            in_failover_queue: false,
        }
    }

    #[test]
    fn provider_type_from_app_type_detects_codex_oauth_metadata() {
        let provider: Provider = serde_json::from_value(json!({
            "id": "codex-oauth",
            "name": "Codex OAuth",
            "settingsConfig": {
                "env": {
                    "ANTHROPIC_BASE_URL": "https://relay.example.com",
                    "ANTHROPIC_AUTH_TOKEN": "token"
                }
            },
            "meta": {
                "providerType": "codex_oauth"
            }
        }))
        .expect("provider should deserialize");

        assert_eq!(
            ProviderType::from_app_type_and_config(&AppType::Claude, &provider),
            ProviderType::CodexOAuth
        );
    }

    #[test]
    fn provider_type_from_app_type_detects_gemini_cli_oauth() {
        let provider = create_provider(json!({
            "env": {
                "GEMINI_API_KEY": "ya29.test-access-token"
            }
        }));

        assert_eq!(
            ProviderType::from_app_type_and_config(&AppType::Gemini, &provider),
            ProviderType::GeminiCli
        );
    }
}