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).
6
7pub mod circuit_breaker;
8pub mod client;
9pub mod models;
10pub mod provider;
11pub mod registry;
12pub mod types;
13
14pub use circuit_breaker::CircuitBreaker;
15pub use client::AiClient;
16pub use models::{AiModel, ModelProvider};
17pub use provider::AiProvider;
18pub use registry::{ModelInfo, ProviderConfig, all_providers, get_provider};
19pub use types::{CreateIssueResponse, CreditsStatus, TriageResponse};
20
21use crate::history::AiStats;
22
23/// Cerebras API base URL (OpenAI-compatible endpoint).
24pub const CEREBRAS_API_URL: &str = "https://api.cerebras.ai/v1/chat/completions";
25
26/// Environment variable for Cerebras API key.
27pub const CEREBRAS_API_KEY_ENV: &str = "CEREBRAS_API_KEY";
28
29/// Gemini API base URL (OpenAI-compatible endpoint).
30pub const GEMINI_API_URL: &str =
31    "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
32
33/// Environment variable for Gemini API key.
34pub const GEMINI_API_KEY_ENV: &str = "GEMINI_API_KEY";
35
36/// Groq API base URL (OpenAI-compatible endpoint).
37pub const GROQ_API_URL: &str = "https://api.groq.com/openai/v1/chat/completions";
38
39/// Environment variable for Groq API key.
40pub const GROQ_API_KEY_ENV: &str = "GROQ_API_KEY";
41
42/// `OpenRouter` API base URL.
43pub const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1/chat/completions";
44
45/// Environment variable for `OpenRouter` API key.
46pub const OPENROUTER_API_KEY_ENV: &str = "OPENROUTER_API_KEY";
47
48/// Response from AI analysis containing both triage data and usage stats.
49#[derive(Debug, Clone)]
50pub struct AiResponse {
51    /// The triage analysis result.
52    pub triage: TriageResponse,
53    /// AI usage statistics.
54    pub stats: AiStats,
55}
56
57/// Checks if a model is in the free tier (no cost).
58/// Free models on `OpenRouter` always have the `:free` suffix.
59#[must_use]
60pub fn is_free_model(model: &str) -> bool {
61    model.ends_with(":free")
62}
63
64/// Creates a formatted GitHub issue using AI assistance.
65///
66/// Takes raw issue title and body, formats them professionally using the configured AI provider.
67/// Returns formatted title, body, and suggested labels.
68///
69/// # Arguments
70///
71/// * `title` - Raw issue title from user
72/// * `body` - Raw issue body/description from user
73/// * `repo` - Repository name for context (owner/repo format)
74///
75/// # Errors
76///
77/// Returns an error if AI formatting fails or API is unavailable.
78pub async fn create_issue(
79    title: &str,
80    body: &str,
81    repo: &str,
82) -> anyhow::Result<CreateIssueResponse> {
83    let config = crate::config::load_config()?;
84
85    // Create generic client for the configured provider
86    let client = AiClient::new(&config.ai.provider, &config.ai)?;
87    client.create_issue(title, body, repo).await
88}