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 "HTTP-Referer: https://github.com/grahamking/ort\r\n",
39 "X-Title: ort\r\n",
40 "\r\n"
41 ),
42 HOST, api_key, USER_AGENT,
43 );
44
45 tls.write_all(prefix.as_bytes())
46 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "write list_models request", e))?;
47 tls.flush()
48 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "flush list_models request", e))?;
49
50 Ok(tls)
51}
52
53pub fn chat_completions(
54 api_key: &str,
55 addrs: Vec<SocketAddr>,
56 json_body: &str,
57) -> OrtResult<buf_read::OrtBufReader<TlsStream<TcpSocket>>> {
58 let tcp = connect(addrs)?;
59
60 let mut tls = TlsStream::connect(tcp, HOST)?;
61
62 let body = json_body.as_bytes();
64 let prefix = format!(
65 concat!(
66 "POST /api/v1/chat/completions HTTP/1.1\r\n",
67 "Content-Type: application/json\r\n",
68 "Accept: text/event-stream\r\n",
69 "Host: {}\r\n",
70 "Authorization: Bearer {}\r\n",
71 "User-Agent: {}\r\n",
72 "HTTP-Referer: https://github.com/grahamking/ort\r\n",
74 "X-Title: ort\r\n",
76 "Content-Length: {}\r\n",
77 "\r\n"
78 ),
79 HOST,
80 api_key,
81 USER_AGENT,
82 body.len()
83 );
84
85 tls.write_all(prefix.as_bytes()).map_err(|e| {
86 ort_from_err(
87 ErrorKind::SocketWriteFailed,
88 "write chat_completions header",
89 e,
90 )
91 })?;
92 tls.write_all(body).map_err(|e| {
93 ort_from_err(
94 ErrorKind::SocketWriteFailed,
95 "write chat_completions body",
96 e,
97 )
98 })?;
99 tls.flush()
100 .map_err(|e| ort_from_err(ErrorKind::SocketWriteFailed, "flush chat_completions", e))?;
101
102 Ok(buf_read::OrtBufReader::new(tls))
103}
104
105#[derive(Debug)]
106pub struct HttpError {
107 status_line: String,
108 body: String,
109}
110
111impl HttpError {
112 fn new(status_line: String, body: String) -> Self {
113 HttpError { status_line, body }
114 }
115
116 fn status(status_line: String) -> Self {
117 HttpError {
118 status_line,
119 body: "".to_string(),
120 }
121 }
122}
123
124impl core::error::Error for HttpError {}
125
126impl fmt::Display for HttpError {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(f, "({}, {})", self.status_line, self.body)
129 }
130}
131
132impl From<HttpError> for OrtError {
133 fn from(err: HttpError) -> OrtError {
134 let c_s = CString::new("\nHTTP ERROR: ".to_string() + &err.to_string()).unwrap();
135 unsafe {
136 libc::write(2, c_s.as_ptr().cast(), c_s.count_bytes());
137 }
138 ort_error(ErrorKind::HttpStatusError, "")
139 }
140}
141
142pub fn skip_header<T: Read + Write>(
146 reader: &mut buf_read::OrtBufReader<TlsStream<T>>,
147) -> Result<bool, HttpError> {
148 let mut buffer = String::with_capacity(16);
149 let status = match reader.read_line(&mut buffer) {
150 Ok(0) => {
151 return Err(HttpError::status("Missing initial status line".to_string()));
152 }
153 Ok(_) => buffer.clone(),
154 Err(err) => {
155 return Err(HttpError::status(format!("Internal TLS error: {err}")));
156 }
157 };
158 let status = status.trim();
159
160 let mut is_chunked = false;
162 buffer.clear();
163 loop {
164 reader
165 .read_line(&mut buffer)
166 .map_err(|err| HttpError::status(format!("Reading response header: {err}")))?;
167 let header = buffer.trim();
168 if header.is_empty() {
169 break;
171 }
172 if header == CHUNKED_HEADER {
173 is_chunked = true;
174 }
175 buffer.clear();
176 }
177
178 if status.trim() != EXPECTED_HTTP_200 {
179 if is_chunked {
181 let _ = reader.read_line(&mut buffer);
184 buffer.clear();
185 }
186 match reader.read_line(&mut buffer) {
187 Ok(_) => {
188 return Err(HttpError::new(
191 status.to_string(),
192 buffer.trim().to_string(),
193 ));
194 }
195 _ => return Err(HttpError::status(status.to_string())),
196 }
197 }
198 Ok(is_chunked)
199}
200
201fn connect(addrs: Vec<SocketAddr>) -> OrtResult<TcpSocket> {
205 for addr in addrs {
210 let addr_v4 = match addr {
211 SocketAddr::V4(v4) => v4,
212 _ => continue,
213 };
214 let sock = TcpSocket::new()?;
215 sock.connect(&addr_v4)?;
216 return Ok(sock);
217 }
229 Err(ort_error(
237 ErrorKind::HttpConnectError,
238 "connect error handling TODO",
239 ))
240}