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 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}