ort_openrouter_cli/net/
http.rs1use core::net::SocketAddr;
8
9extern crate alloc;
10use alloc::ffi::CString;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13
14use crate::{
15 Context, ErrorKind, OrtError, OrtResult, Read, TcpSocket, TlsStream, Write, common::buf_read,
16 ort_error,
17};
18use crate::{syscall, utils};
19
20const EXPECTED_HTTP_200: &str = "HTTP/1.1 200 OK";
21const CHUNKED_HEADER: &str = "Transfer-Encoding: chunked";
22const CONTENT_LENGTH_0: &str = "Content-Length: 0";
23
24const POST: &[u8] = "POST ".as_bytes();
25const GET: &[u8] = "GET ".as_bytes();
26const HTTP_1_1: &[u8] = " HTTP/1.1\r\n".as_bytes();
27const HOST_HEADER: &[u8] = "Host: ".as_bytes();
28const CONTENT_LENGTH_HEADER: &[u8] = "Content-Length: ".as_bytes();
29const CRLF: &[u8] = "\r\n".as_bytes();
30
31const LIST_REQ_MIDDLE: &[u8] = concat!(
33 "Accept: application/json\r\n",
34 "User-Agent: ",
35 env!("CARGO_PKG_NAME"),
36 "/",
37 env!("CARGO_PKG_VERSION"),
38 "\r\n",
39 "HTTP-Referer: https://github.com/grahamking/ort\r\n",
40 "X-Title: ort\r\n",
41 "Authorization: Bearer "
42)
43.as_bytes();
44
45pub fn list_models(
46 api_key: &str,
47 host: &'static str,
48 list_url: &'static str,
49 addrs: Vec<SocketAddr>,
50) -> OrtResult<TlsStream<TcpSocket>> {
51 let tcp = connect(addrs)?;
52 let mut tls = TlsStream::connect(tcp, host)?;
53
54 let mut req = [0u8; 384];
57
58 let mut start = 0;
60 let mut end = GET.len();
61 req[start..end].copy_from_slice(GET);
62 start = end;
63 end += list_url.len();
64 req[start..end].copy_from_slice(list_url.as_bytes());
65 start = end;
66 end += HTTP_1_1.len();
67 req[start..end].copy_from_slice(HTTP_1_1);
68
69 start = end;
71 end += HOST_HEADER.len();
72 req[start..end].copy_from_slice(HOST_HEADER);
73 start = end;
74 end += host.len();
75 req[start..end].copy_from_slice(host.as_bytes());
76 start = end;
77 end += CRLF.len();
78 req[start..end].copy_from_slice(CRLF);
79
80 start = end;
82 end += LIST_REQ_MIDDLE.len();
83 req[start..end].copy_from_slice(LIST_REQ_MIDDLE);
84
85 start = end;
88 end += api_key.len();
89 req[start..end].copy_from_slice(api_key.as_bytes());
90 start = end;
91 end += CRLF.len();
92 req[start..end].copy_from_slice(CRLF);
93 start = end;
94 end += CRLF.len();
95 req[start..end].copy_from_slice(CRLF);
96
97 tls.write_all(&req[..end])
98 .context("write list_models request")?;
99 tls.flush().context("flush list_models request")?;
100
101 Ok(tls)
102}
103
104const CHAT_REQ_MIDDLE: &[u8] = concat!(
105 "Content-Type: application/json\r\n",
106 "Accept: text/event-stream\r\n",
107 "User-Agent: ",
108 env!("CARGO_PKG_NAME"),
109 "/",
110 env!("CARGO_PKG_VERSION"),
111 "\r\n",
112 "HTTP-Referer: https://github.com/grahamking/ort\r\n",
114 "X-Title: ort\r\n",
116 "Authorization: Bearer "
117)
118.as_bytes();
119
120pub fn chat_completions(
121 api_key: &str,
122 host: &'static str,
123 chat_completions_url: &'static str,
124 addrs: Vec<SocketAddr>,
125 json_body: &str,
126) -> OrtResult<buf_read::OrtBufReader<TlsStream<TcpSocket>>> {
127 let tcp = connect(addrs)?;
128 let mut tls = TlsStream::connect(tcp, host)?;
129
130 let body = json_body.as_bytes();
131
132 let mut req = [0u8; 512];
135
136 let mut start = 0;
138 let mut end = POST.len();
139 req[start..end].copy_from_slice(POST);
140 start = end;
141 end += chat_completions_url.len();
142 req[start..end].copy_from_slice(chat_completions_url.as_bytes());
143 start = end;
144 end += HTTP_1_1.len();
145 req[start..end].copy_from_slice(HTTP_1_1);
146
147 start = end;
149 end += HOST_HEADER.len();
150 req[start..end].copy_from_slice(HOST_HEADER);
151 start = end;
152 end += host.len();
153 req[start..end].copy_from_slice(host.as_bytes());
154 start = end;
155 end += CRLF.len();
156 req[start..end].copy_from_slice(CRLF);
157
158 start = end;
160 end += CONTENT_LENGTH_HEADER.len();
161 req[start..end].copy_from_slice(CONTENT_LENGTH_HEADER);
162 let mut body_len_buf: [u8; 16] = [0; 16];
163 let buf_len = utils::to_ascii(body.len(), &mut body_len_buf[..]);
164 start = end;
165 end += buf_len - 2;
167 req[start..end].copy_from_slice(&body_len_buf[..buf_len - 2]);
168 start = end;
169 end += CRLF.len();
170 req[start..end].copy_from_slice(CRLF);
171
172 start = end;
174 end += CHAT_REQ_MIDDLE.len();
175 req[start..end].copy_from_slice(CHAT_REQ_MIDDLE);
176
177 start = end;
180 end += api_key.len();
181 req[start..end].copy_from_slice(api_key.as_bytes());
182 start = end;
183 end += CRLF.len();
184 req[start..end].copy_from_slice(CRLF);
185 start = end;
186 end += CRLF.len();
187 req[start..end].copy_from_slice(CRLF);
188
189 tls.write_all(&req[..end])
193 .context("write chat_completions header")?;
194 tls.write_all(body).context("write chat_completions body")?;
195 tls.flush().context("flush chat_completions")?;
196
197 Ok(buf_read::OrtBufReader::new(tls))
198}
199
200#[derive(Debug)]
201pub struct HttpError {
202 status_line: String,
203 body: String,
204}
205
206impl HttpError {
207 pub(crate) fn as_string(&self) -> String {
208 let mut msg = String::with_capacity(16 + self.status_line.len() + self.body.len());
209 msg.push_str("\nHTTP ERROR: ");
210 msg.push_str(&self.status_line);
211 msg.push_str(", ");
212 msg.push_str(&self.body);
213 msg.push('\0');
214 msg
215 }
216
217 fn new(status_line: String, body: String) -> Self {
218 HttpError { status_line, body }
219 }
220
221 fn status(status_line: String) -> Self {
222 HttpError {
223 status_line,
224 body: "".to_string(),
225 }
226 }
227}
228
229impl From<HttpError> for OrtError {
230 fn from(err: HttpError) -> OrtError {
231 let c_s = unsafe { CString::from_vec_with_nul_unchecked(err.as_string().into_bytes()) };
232 syscall::write(2, c_s.as_ptr().cast(), c_s.count_bytes());
233 ort_error(ErrorKind::HttpStatusError, "")
234 }
235}
236
237pub fn skip_header<T: Read + Write>(
241 reader: &mut buf_read::OrtBufReader<TlsStream<T>>,
242) -> Result<bool, HttpError> {
243 let mut buffer = String::with_capacity(512);
244 let status = match reader.read_line(&mut buffer) {
245 Ok(0) => {
246 return Err(HttpError::status("Missing initial status line".to_string()));
247 }
248 Ok(_) => buffer.clone(),
249 Err(err) => {
250 return Err(HttpError::status(
251 "Internal TLS error: ".to_string() + &err.as_string(),
252 ));
253 }
254 };
255 let status = status.trim();
256 let mut is_chunked = false;
260 let mut has_content = true;
261 buffer.clear();
262 loop {
263 reader.read_line(&mut buffer).map_err(|err| {
264 HttpError::status("Reading response header: ".to_string() + &err.as_string())
265 })?;
266 let header = buffer.trim();
267 if header.is_empty() {
268 break;
270 }
271 if header == CHUNKED_HEADER {
272 is_chunked = true;
273 }
274 if header == CONTENT_LENGTH_0 {
275 has_content = false;
276 }
277 buffer.clear();
280 }
281
282 if status.trim() != EXPECTED_HTTP_200 {
283 if is_chunked {
285 let _ = reader.read_line(&mut buffer);
288 buffer.clear();
289 }
290 if !has_content {
291 return Err(HttpError::status(status.to_string()));
292 }
293 match reader.read_line(&mut buffer) {
294 Ok(_) => {
295 return Err(HttpError::new(
298 status.to_string(),
299 buffer.trim().to_string(),
300 ));
301 }
302 _ => return Err(HttpError::status(status.to_string())),
303 }
304 }
305 Ok(is_chunked)
306}
307
308fn connect(addrs: Vec<SocketAddr>) -> OrtResult<TcpSocket> {
312 for addr in addrs {
317 let addr_v4 = match addr {
318 SocketAddr::V4(v4) => v4,
319 _ => continue,
320 };
321 let sock = TcpSocket::new()?;
322 sock.connect(&addr_v4)?;
323 return Ok(sock);
324 }
336 Err(ort_error(
342 ErrorKind::HttpConnectError,
343 "connect error handling TODO",
344 ))
345}