1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
use tracing::info;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::lang;
pub struct OpenRouterClient {
api_key: String,
model: String,
api_url: String,
client: Client,
}
#[derive(Serialize)]
struct ChatRequest {
model: String,
messages: Vec<Message>,
max_tokens: u32,
}
#[derive(Serialize)]
struct Message {
role: String,
content: String,
}
#[derive(Deserialize)]
struct ChatResponse {
choices: Vec<Choice>,
}
#[derive(Deserialize)]
struct Choice {
message: MessageContent,
}
#[derive(Deserialize)]
struct MessageContent {
content: String,
}
impl OpenRouterClient {
/// `api_url` is the full URL including `/chat/completions` path.
pub fn new(api_url: String, api_key: String, model: String) -> Self {
info!("LLM client: url={api_url}, model={model}");
Self {
api_key,
model,
api_url,
client: Client::new(),
}
}
async fn do_request(&self, body: &ChatRequest) -> Result<String> {
let mut req = self
.client
.post(&self.api_url)
.header("Content-Type", "application/json")
.json(body);
if !self.api_key.is_empty() {
req = req.header("Authorization", format!("Bearer {}", self.api_key));
}
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(crate::error::Error::Custom(lang::fmt_llm_err(
&self.api_url,
&status,
&text,
)));
}
let chat_resp: ChatResponse = resp.json().await?;
Ok(chat_resp
.choices
.into_iter()
.next()
.map(|c| c.message.content)
.unwrap_or_default())
}
pub async fn chat(&self, system_prompt: &str, user_message: &str) -> Result<String> {
let body = ChatRequest {
model: self.model.clone(),
messages: vec![
Message {
role: "system".into(),
content: system_prompt.to_string(),
},
Message {
role: "user".into(),
content: user_message.to_string(),
},
],
max_tokens: 500,
};
self.do_request(&body).await
}
pub async fn chat_with_history(
&self,
system_prompt: &str,
messages: Vec<(String, String)>, // (role, content) pairs
) -> Result<String> {
let mut msg_vec = vec![Message {
role: "system".into(),
content: system_prompt.to_string(),
}];
for (role, content) in messages {
msg_vec.push(Message { role, content });
}
let body = ChatRequest {
model: self.model.clone(),
messages: msg_vec,
max_tokens: 500,
};
self.do_request(&body).await
}
}
|