Skip to main content

aptu_core/ai/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! AI integration module.
4//!
5//! Provides AI-assisted issue triage using multiple AI providers (Gemini, `OpenRouter`, Groq, Cerebras, Zenmux, Z.AI).
6
7pub mod circuit_breaker;
8pub mod client;
9pub mod context;
10pub mod dep_enrichment;
11pub mod models;
12pub mod prompts;
13pub mod provider;
14pub mod registry;
15pub mod review_context;
16pub mod types;
17
18pub use circuit_breaker::CircuitBreaker;
19pub use client::{AiClient, AuthMethod};
20pub use dep_enrichment::enrich_dep_releases;
21pub use models::{AiModel, ModelProvider};
22pub use provider::AiProvider;
23pub use registry::{PROVIDER_ANTHROPIC, ProviderConfig, all_providers, get_provider};
24pub use types::{CreateIssueResponse, CreditsStatus, DepReleaseNote, TriageResponse};
25
26use crate::history::AiStats;
27
28/// Response from AI analysis containing both triage data and usage stats.
29#[derive(Debug, Clone)]
30pub struct AiResponse {
31    /// The triage analysis result.
32    pub triage: TriageResponse,
33    /// AI usage statistics.
34    pub stats: AiStats,
35}
36
37/// Checks if a model is in the free tier (no cost).
38/// Free models on `OpenRouter` always have the `:free` suffix.
39#[must_use]
40pub fn is_free_model(model: &str) -> bool {
41    model.ends_with(":free")
42}
43
44/// Resolves Anthropic credentials with OAuth fallback.
45///
46/// For the Anthropic provider, attempts to use Claude OAuth credentials in this order:
47/// 1. Existing token in OS keyring
48/// 2. ~/.claude/credentials.json file
49/// 3. Environment variable (fallback)
50///
51/// Returns `Some(client)` if credentials were found via OAuth or env var,
52/// `None` if no credentials were available.
53#[must_use]
54pub fn resolve_anthropic_credential(ai_config: &crate::config::AiConfig) -> Option<AiClient> {
55    // Try keyring first
56    if let Ok(Some(client)) = AiClient::from_keyring_oauth(ai_config) {
57        return Some(client);
58    }
59
60    // Try credentials file
61    if let Ok(Some(client)) = AiClient::from_claude_credentials(ai_config) {
62        return Some(client);
63    }
64
65    // Fall back to environment variable
66    AiClient::new(PROVIDER_ANTHROPIC, ai_config).ok()
67}
68
69/// Sets up the primary AI client with credential resolution.
70///
71/// For the Anthropic provider, attempts to use Claude OAuth credentials in this order:
72/// 1. Existing token in OS keyring
73/// 2. ~/.claude/credentials.json file
74/// 3. Environment variable (fallback)
75///
76/// For other providers, uses the standard environment variable path.
77///
78/// # Errors
79///
80/// Returns an error if client creation fails.
81pub fn setup_primary_client(config: &crate::config::AppConfig) -> anyhow::Result<AiClient> {
82    // For Anthropic, delegate to centralized credential resolution
83    if config.ai.provider == PROVIDER_ANTHROPIC
84        && let Some(client) = resolve_anthropic_credential(&config.ai)
85    {
86        return Ok(client);
87    }
88
89    // Fall back to environment variable for non-Anthropic providers
90    AiClient::new(&config.ai.provider, &config.ai)
91}
92
93/// Creates a formatted GitHub issue using AI assistance.
94///
95/// Takes raw issue title and body, formats them professionally using the configured AI provider.
96/// Returns formatted title, body, and suggested labels.
97///
98/// # Arguments
99///
100/// * `title` - Raw issue title from user
101/// * `body` - Raw issue body/description from user
102/// * `repo` - Repository name for context (owner/repo format)
103///
104/// # Errors
105///
106/// Returns an error if AI formatting fails or API is unavailable.
107pub async fn create_issue(
108    title: &str,
109    body: &str,
110    repo: &str,
111) -> anyhow::Result<(CreateIssueResponse, AiStats)> {
112    let config = crate::config::load_config()?;
113
114    // Create generic client for the configured provider
115    let client = setup_primary_client(&config)?;
116    client.create_issue(title, body, repo).await
117}