Skip to main content

git_comma/
openrouter.rs

1use serde::Deserialize;
2use std::fmt;
3
4#[derive(Debug, Deserialize)]
5pub struct Model {
6    pub id: String,
7}
8
9#[derive(Debug, Deserialize)]
10pub struct ModelsResponse {
11    pub data: Vec<Model>,
12}
13
14#[derive(Debug, Deserialize)]
15struct MessageContent {
16    content: Option<String>,
17}
18
19#[derive(Debug, Deserialize)]
20struct Choice {
21    message: MessageContent,
22}
23
24#[derive(Debug, Deserialize)]
25struct ChatResponse {
26    choices: Vec<Choice>,
27}
28
29#[derive(Debug)]
30pub enum ApiError {
31    Unauthorized,
32    Forbidden,
33    RateLimited,
34    HttpError(u16),
35    NetworkError(String),
36    ParseError,
37    EmptyResponse,
38}
39
40impl fmt::Display for ApiError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            ApiError::Unauthorized => write!(f, "Unauthorized"),
44            ApiError::Forbidden => write!(f, "Forbidden"),
45            ApiError::RateLimited => write!(f, "Rate limited"),
46            ApiError::HttpError(code) => write!(f, "HTTP error {}", code),
47            ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
48            ApiError::ParseError => write!(f, "Parse error"),
49            ApiError::EmptyResponse => write!(f, "Empty response"),
50        }
51    }
52}
53
54pub struct Client {
55    api_key: String,
56}
57
58impl Client {
59    pub fn new(api_key: String) -> Self {
60        Self { api_key }
61    }
62
63    pub fn fetch_models(&self) -> Result<Vec<Model>, ApiError> {
64        let mut resp = ureq::get("https://openrouter.ai/api/v1/models")
65            .header("Authorization", &format!("Bearer {}", self.api_key))
66            .call()
67            .map_err(|e: ureq::Error| match e {
68                ureq::Error::StatusCode(code) => ApiError::HttpError(code),
69                other => ApiError::NetworkError(other.to_string()),
70            })?;
71
72        let status_u16: u16 = resp.status().into();
73        if status_u16 != 200 {
74            match status_u16 {
75                401 => return Err(ApiError::Unauthorized),
76                403 => return Err(ApiError::Forbidden),
77                429 => return Err(ApiError::RateLimited),
78                code => return Err(ApiError::HttpError(code)),
79            }
80        }
81
82        let models_resp: ModelsResponse = resp
83            .body_mut()
84            .read_json()
85            .map_err(|_| ApiError::ParseError)?;
86
87        if models_resp.data.is_empty() {
88            return Err(ApiError::EmptyResponse);
89        }
90
91        Ok(models_resp.data)
92    }
93
94    /// Generate commit message from diff content via OpenRouter chat completions.
95    pub fn generate_commit_message(&self, payload: &serde_json::Value) -> Result<String, ApiError> {
96        let mut resp = ureq::post("https://openrouter.ai/api/v1/chat/completions")
97            .header("Authorization", &format!("Bearer {}", self.api_key))
98            .header("Content-Type", "application/json")
99            .send_json(payload)
100            .map_err(|e: ureq::Error| match e {
101                ureq::Error::StatusCode(code) => ApiError::HttpError(code),
102                other => ApiError::NetworkError(other.to_string()),
103            })?;
104
105        let status_u16: u16 = resp.status().into();
106        match status_u16 {
107            200 => {}
108            401 => return Err(ApiError::Unauthorized),
109            403 => return Err(ApiError::Forbidden),
110            429 => return Err(ApiError::RateLimited),
111            code => return Err(ApiError::HttpError(code)),
112        }
113
114        let chat_resp: ChatResponse = resp
115            .body_mut()
116            .read_json()
117            .map_err(|_| ApiError::ParseError)?;
118
119        chat_resp
120            .choices
121            .into_iter()
122            .next()
123            .and_then(|c| c.message.content)
124            .filter(|s| !s.trim().is_empty())
125            .ok_or(ApiError::EmptyResponse)
126    }
127}