pub mod create;
pub mod http;
pub mod label;
pub mod parse;
pub mod review;
pub mod triage;
use anyhow::Result;
use async_trait::async_trait;
use reqwest::Client;
use secrecy::SecretString;
use crate::ai::registry::ProviderConfig;
use crate::ai::types::{
ChatCompletionRequest, ChatCompletionResponse, CreateIssueResponse, IssueDetails,
PrReviewResponse,
};
use crate::history::AiStats;
pub(crate) use crate::ai::provider::parse::{SCHEMA_PREAMBLE, sanitize_prompt_field};
pub const MAX_BODY_LENGTH: usize = 4000;
pub const MAX_COMMENTS: usize = 5;
pub const MAX_FILES: usize = 20;
pub const MAX_LABELS: usize = 30;
pub const MAX_MILESTONES: usize = 10;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait AiProvider: Send + Sync {
fn config(&self) -> &ProviderConfig;
fn name(&self) -> &str {
self.config().name
}
fn api_url(&self) -> &str {
self.config().api_url
}
fn api_key_env(&self) -> &str {
self.config().api_key_env
}
fn http_client(&self) -> &Client;
fn api_key(&self) -> &SecretString;
fn model(&self) -> &str {
self.config().model
}
fn max_tokens(&self) -> u32 {
self.config().max_tokens
}
fn temperature(&self) -> f32 {
self.config().temperature
}
fn is_anthropic(&self) -> bool {
self.name() == crate::ai::registry::PROVIDER_ANTHROPIC
}
fn max_attempts(&self) -> u32 {
3
}
fn circuit_breaker(&self) -> Option<&crate::ai::CircuitBreaker> {
None
}
fn build_headers(&self) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(val) = "application/json".parse() {
headers.insert("Content-Type", val);
}
headers
}
fn validate_model(&self) -> Result<()> {
Ok(())
}
fn custom_guidance(&self) -> Option<&str> {
None
}
#[allow(private_interfaces)]
async fn send_request_inner(
&self,
request: &ChatCompletionRequest,
) -> Result<ChatCompletionResponse> {
self::http::send_request_inner(self, request).await
}
#[allow(private_interfaces)]
async fn send_and_parse<T: serde::de::DeserializeOwned + Send>(
&self,
request: &ChatCompletionRequest,
) -> Result<(T, AiStats, Vec<String>)> {
self::http::send_and_parse(self, request).await
}
async fn analyze_issue(&self, issue: &IssueDetails) -> Result<crate::ai::AiResponse> {
self::triage::analyze_issue(self, issue).await
}
#[must_use]
fn build_system_prompt(custom_guidance: Option<&str>) -> String {
self::triage::build_system_prompt(custom_guidance)
}
#[must_use]
fn build_create_system_prompt(custom_guidance: Option<&str>) -> String {
self::create::build_create_system_prompt_fn(custom_guidance)
}
async fn create_issue(
&self,
title: &str,
body: &str,
repo: &str,
) -> Result<(CreateIssueResponse, AiStats)> {
self::create::create_issue(self, title, body, repo).await
}
#[must_use]
fn estimate_pr_size(
pr: &crate::ai::types::PrDetails,
ast_context: &str,
call_graph: &str,
) -> usize {
self::review::estimate_pr_size(pr, ast_context, call_graph)
}
#[allow(unused_assignments)]
async fn review_pr(
&self,
ctx: crate::ai::review_context::ReviewContext,
review_config: &crate::config::ReviewConfig,
) -> Result<(PrReviewResponse, AiStats, Vec<String>)> {
self::review::review_pr(self, ctx, review_config).await
}
async fn suggest_pr_labels(
&self,
title: &str,
body: &str,
file_paths: &[String],
) -> Result<(Vec<String>, AiStats)> {
self::label::suggest_pr_labels(self, title, body, file_paths).await
}
#[must_use]
fn build_pr_review_system_prompt(custom_guidance: Option<&str>) -> String {
self::review::build_pr_review_system_prompt_fn(custom_guidance)
}
#[must_use]
fn build_pr_review_user_prompt(ctx: &mut crate::ai::review_context::ReviewContext) -> String {
self::review::build_pr_review_user_prompt(ctx)
}
#[must_use]
fn build_pr_label_system_prompt(custom_guidance: Option<&str>) -> String {
self::label::build_pr_label_system_prompt_fn(custom_guidance)
}
#[must_use]
fn build_pr_label_user_prompt(title: &str, body: &str, file_paths: &[String]) -> String {
self::label::build_pr_label_user_prompt(title, body, file_paths)
}
}
#[cfg(test)]
pub(crate) mod test_utils {
use super::*;
pub(crate) static TEST_PROVIDER_CONFIG: ProviderConfig = ProviderConfig {
name: "test",
display_name: "Test",
api_url: "https://test.example.com",
api_key_env: "TEST_API_KEY",
model: "test-model",
max_tokens: 2048,
temperature: 0.3,
};
#[derive(Debug, serde::Deserialize)]
pub(crate) struct ErrorTestResponse {
pub(crate) _message: String,
}
pub(crate) struct TestProvider;
impl AiProvider for TestProvider {
fn config(&self) -> &ProviderConfig {
&TEST_PROVIDER_CONFIG
}
fn http_client(&self) -> &Client {
unimplemented!()
}
fn api_key(&self) -> &SecretString {
unimplemented!()
}
}
}