ort_openrouter_cli/net/
http.rs1use core::fmt;
8use core::net::SocketAddr;
9
10extern crate alloc;
11use alloc::ffi::CString;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use crate::libc;
17use crate::{
18 ErrorKind, OrtError, OrtResult, Read, TcpSocket, TlsStream, Write, common::buf_read, ort_error,
19 ort_from_err,
20};
21
22const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
23const HOST: &str = "openrouter.ai";
24const EXPECTED_HTTP_200: &str = "HTTP/1.1 200 OK";
25const CHUNKED_HEADER: &str = "Transfer-Encoding: chunked";
26
27pub fn list_models(api_key: &str, addrs: Vec<SocketAddr>) -> OrtResult<TlsStream<TcpSocket>> {
28 let tcp = connect(addrs)?;
29 let mut tls = TlsStream::connect(tcp, HOST)?;
30
31 let prefix = format!(
32 concat!(
33 "GET /api/v1/models HTTP/1.1\r\n",
34 "Accept: application/json\r\n",
35 "Host: {}\r\n",
36 "Authorization: Bearer {}\r\n",
37 "User-Agent: {}\r\n",
38 "\r\n"
39 ),
40 HOST, api_key, USER_AGENT,
41 );
42
43 tls.write_all(prefix.as_bytes())
44 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "write list_models request", e))?;
45 tls.flush()
46 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "flush list_models request", e))?;
47
48 Ok(tls)
49}
50
51pub fn chat_completions(
52 api_key: &str,
53 addrs: Vec<SocketAddr>,
54 json_body: &str,
55) -> OrtResult<buf_read::OrtBufReader<TlsStream<TcpSocket>>> {
56 let tcp = connect(addrs)?;
57
58 let mut tls = TlsStream::connect(tcp, HOST)?;
59
60 let body = json_body.as_bytes();
62 let prefix = format!(
63 concat!(
64 "POST /api/v1/chat/completions HTTP/1.1\r\n",
65 "Content-Type: application/json\r\n",
66 "Accept: text/event-stream\r\n",
67 "Host: {}\r\n",
68 "Authorization: Bearer {}\r\n",
69 "User-Agent: {}\r\n",
70 "Content-Length: {}\r\n",
71 "\r\n"
72 ),
73 HOST,
74 api_key,
75 USER_AGENT,
76 body.len()
77 );
78
79 tls.write_all(prefix.as_bytes()).map_err(|e| {
80 ort_from_err(
81 ErrorKind::SocketWriteFailed,
82 "write chat_completions header",
83 e,
84 )
85 })?;
86 tls.write_all(body).map_err(|e| {
87 ort_from_err(
88 ErrorKind::SocketWriteFailed,
89 "write chat_completions body",
90 e,
91 )
92 })?;
93 tls.flush()
94 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "flush chat_completions", e))?;
95
96 Ok(buf_read::OrtBufReader::new(tls))
97}
98
99#[derive(Debug)]
100pub struct HttpError {
101 status_line: String,
102 body: String,
103}
104
105impl HttpError {
106 fn new(status_line: String, body: String) -> Self {
107 HttpError { status_line, body }
108 }
109
110 fn status(status_line: String) -> Self {
111 HttpError {
112 status_line,
113 body: "".to_string(),
114 }
115 }
116}
117
118impl core::error::Error for HttpError {}
119
120impl fmt::Display for HttpError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "({}, {})", self.status_line, self.body)
123 }
124}
125
126impl From<HttpError> for OrtError {
127 fn from(err: HttpError) -> OrtError {
128 let c_s = CString::new("\nHTTP ERROR: ".to_string() + &err.to_string()).unwrap();
129 unsafe {
130 libc::write(2, c_s.as_ptr().cast(), c_s.count_bytes());
131 }
132 ort_error(ErrorKind::HttpStatusError, "")
133 }
134}
135
136pub fn skip_header<T: Read + Write>(
140 reader: &mut buf_read::OrtBufReader<TlsStream<T>>,
141) -> Result<bool, HttpError> {
142 let mut buffer = String::with_capacity(16);
143 let status = match reader.read_line(&mut buffer) {
144 Ok(0) => {
145 return Err(HttpError::status("Missing initial status line".to_string()));
146 }
147 Ok(_) => buffer.clone(),
148 Err(err) => {
149 return Err(HttpError::status(format!("Internal TLS error: {err}")));
150 }
151 };
152 let status = status.trim();
153
154 let mut is_chunked = false;
156 buffer.clear();
157 loop {
158 reader
159 .read_line(&mut buffer)
160 .map_err(|err| HttpError::status(format!("Reading response header: {err}")))?;
161 let header = buffer.trim();
162 if header.is_empty() {
163 break;
165 }
166 if header == CHUNKED_HEADER {
167 is_chunked = true;
168 }
169 buffer.clear();
170 }
171
172 if status.trim() != EXPECTED_HTTP_200 {
173 if is_chunked {
175 let _ = reader.read_line(&mut buffer);
178 buffer.clear();
179 }
180 match reader.read_line(&mut buffer) {
181 Ok(_) => {
182 return Err(HttpError::new(
185 status.to_string(),
186 buffer.trim().to_string(),
187 ));
188 }
189 _ => return Err(HttpError::status(status.to_string())),
190 }
191 }
192 Ok(is_chunked)
193}
194
195fn connect(addrs: Vec<SocketAddr>) -> OrtResult<TcpSocket> {
199 for addr in addrs {
204 let addr_v4 = match addr {
205 SocketAddr::V4(v4) => v4,
206 _ => continue,
207 };
208 let sock = TcpSocket::new()?;
209 sock.connect(&addr_v4)?;
210 return Ok(sock);
211 }
223 Err(ort_error(
231 ErrorKind::HttpConnectError,
232 "connect error handling TODO",
233 ))
234}