magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use super::{
    ANTHROPIC_PROVIDER, AnthropicProvider, CLAUDE_SUBSCRIPTION_PROVIDER, OPENAI_CODEX_PROVIDER,
    OpenAiCodexProvider, OpenAiCompatibleProvider, Provider, ProviderSelection,
    ReqwestHttpTransport, claude_subscription::ClaudeSubscriptionProvider,
};
use crate::{
    auth::ProviderCredential,
    config::{CustomProviderConfig, EffectiveConfig, Settings},
    fast::FastWorkload,
};
use anyhow::Result;
use std::{path::Path, sync::Arc};

pub(crate) fn supported_custom_provider<'a>(
    config: &'a EffectiveConfig,
    selection: &ProviderSelection,
) -> Result<Option<&'a CustomProviderConfig>> {
    crate::config::reject_retired_provider_selection(&selection.provider, &selection.model)?;
    let selected_custom_provider = config.custom_providers.get(&selection.provider);
    if selection.provider.as_str() != OPENAI_CODEX_PROVIDER
        && selection.provider.as_str() != ANTHROPIC_PROVIDER
        && selection.provider.as_str() != CLAUDE_SUBSCRIPTION_PROVIDER
        && selected_custom_provider.is_none()
    {
        anyhow::bail!(
            "unsupported provider '{}'; supported providers: '{}', '{}', '{}' and configured custom providers",
            selection.provider,
            OPENAI_CODEX_PROVIDER,
            ANTHROPIC_PROVIDER,
            CLAUDE_SUBSCRIPTION_PROVIDER
        );
    }
    Ok(selected_custom_provider)
}

fn custom_provider_from_config<T>(
    paths: &crate::config::McPaths,
    provider_id: &str,
    model: &str,
    api_key: Option<String>,
    custom: &CustomProviderConfig,
    transport: T,
    fast: crate::fast::FastRequestState,
) -> OpenAiCompatibleProvider<T> {
    OpenAiCompatibleProvider::custom(
        provider_id.to_string(),
        model.to_string(),
        api_key,
        custom.base_url.clone(),
        custom.use_responses_endpoint,
        transport,
    )
    .with_request_headers(custom.request_headers.clone())
    .with_text_verbosity_support(custom.supports_text_verbosity)
    .with_reasoning_protocol(
        custom.reasoning_protocol,
        crate::model_catalog::cached_model_max_output_tokens(paths, provider_id, model),
    )
    .with_service_tier(fast.service_tier().map(str::to_string))
}

pub(crate) fn provider_from_selection_with_settings_for_workload(
    config: &EffectiveConfig,
    selection: &ProviderSelection,
    cwd: &Path,
    settings: &Settings,
    workload: FastWorkload,
) -> Result<Arc<dyn Provider>> {
    provider_from_selection_with_settings_for_workload_with_auth(
        config,
        selection,
        cwd,
        settings,
        workload,
        |config| config.resolve_provider_auth_for_runtime(),
    )
}

pub(crate) fn provider_from_selection_with_settings_for_workload_with_resolved_auth(
    config: &EffectiveConfig,
    selection: &ProviderSelection,
    cwd: &Path,
    settings: &Settings,
    workload: FastWorkload,
    auth: ProviderCredential,
) -> Result<Arc<dyn Provider>> {
    provider_from_selection_with_settings_for_workload_with_auth(
        config,
        selection,
        cwd,
        settings,
        workload,
        move |_| Ok(auth),
    )
}

fn provider_from_selection_with_settings_for_workload_with_auth(
    config: &EffectiveConfig,
    selection: &ProviderSelection,
    cwd: &Path,
    settings: &Settings,
    workload: FastWorkload,
    resolve_auth: impl FnOnce(&EffectiveConfig) -> Result<ProviderCredential>,
) -> Result<Arc<dyn Provider>> {
    let selected_custom_provider = supported_custom_provider(config, selection)?;
    let fast = crate::fast::resolve_fast_capability(
        settings,
        &selection.provider,
        &selection.model,
        &config.paths,
        selected_custom_provider,
        workload,
    );
    let auth = resolve_auth(config)?;
    match (selection.provider.as_str(), auth) {
        (OPENAI_CODEX_PROVIDER, ProviderCredential::OAuth { access, account_id }) => {
            let paths = config.paths.clone();
            Ok(Arc::new(
                OpenAiCodexProvider::new(
                    selection.model.clone(),
                    access,
                    account_id,
                    ReqwestHttpTransport,
                )
                .with_service_tier(fast.service_tier().map(str::to_string))
                .with_store_auth_refresh(paths),
            ))
        }
        (OPENAI_CODEX_PROVIDER, ProviderCredential::ApiKey { .. }) => anyhow::bail!(
            "provider 'openai-codex' requires provider-keyed OAuth auth; refusing to send API-key/runtime token to Codex transport"
        ),
        (OPENAI_CODEX_PROVIDER, ProviderCredential::NoAuth) => anyhow::bail!(
            "provider 'openai-codex' requires provider-keyed OAuth auth; refusing no-auth configuration"
        ),
        (CLAUDE_SUBSCRIPTION_PROVIDER, ProviderCredential::NoAuth) => {
            if let Err(reason) = crate::providers::claude_subscription::probe_cli_subscription() {
                anyhow::bail!("Claude CLI subscription unavailable: {reason}");
            }
            Ok(Arc::new(ClaudeSubscriptionProvider {
                cwd: cwd.to_path_buf(),
            }))
        }
        (CLAUDE_SUBSCRIPTION_PROVIDER, _) => anyhow::bail!(
            "Claude subscription requires CLI-managed claude.ai login; API keys and stored OAuth tokens are not used"
        ),
        (ANTHROPIC_PROVIDER, ProviderCredential::ApiKey { key }) => {
            let max_output_tokens = crate::model_catalog::cached_model_max_output_tokens(
                &config.paths,
                &selection.provider,
                &selection.model,
            );
            let thinking_level = crate::thinking::resolve_thinking_level(
                &crate::thinking::available_thinking_levels(
                    &selection.provider,
                    &selection.model,
                    crate::model_catalog::cached_model_thinking_metadata(
                        &config.paths,
                        &selection.provider,
                        &selection.model,
                    )
                    .as_ref(),
                    crate::thinking::ThinkingCapabilityScope::BuiltIn,
                ),
                config.thinking_level,
            );
            Ok(Arc::new(
                AnthropicProvider::new(selection.model.clone(), key, ReqwestHttpTransport)
                    .with_cache_ttl(settings.anthropic_cache_ttl)
                    .with_max_output_tokens(max_output_tokens)
                    .with_thinking_level(thinking_level),
            ))
        }
        (ANTHROPIC_PROVIDER, ProviderCredential::OAuth { .. }) => {
            anyhow::bail!("provider 'anthropic' requires Anthropic API-key auth, not OAuth token")
        }
        (ANTHROPIC_PROVIDER, ProviderCredential::NoAuth) => anyhow::bail!(
            "provider 'anthropic' requires Anthropic API-key auth; refusing no-auth configuration"
        ),
        (provider_id, ProviderCredential::ApiKey { key }) => {
            let Some(custom) = selected_custom_provider else {
                unreachable!("provider support checked above")
            };
            Ok(Arc::new(custom_provider_from_config(
                &config.paths,
                provider_id,
                &selection.model,
                Some(key),
                custom,
                ReqwestHttpTransport,
                fast.clone(),
            )))
        }
        (provider_id, ProviderCredential::NoAuth) => {
            let Some(custom) = selected_custom_provider else {
                unreachable!("provider support checked above")
            };
            Ok(Arc::new(custom_provider_from_config(
                &config.paths,
                provider_id,
                &selection.model,
                None,
                custom,
                ReqwestHttpTransport,
                fast,
            )))
        }
        (provider_id, ProviderCredential::OAuth { .. }) => anyhow::bail!(
            "provider '{provider_id}' requires custom-provider auth mode, not OAuth token"
        ),
    }
}