ort_openrouter_cli/output/
writer.rs1extern crate alloc;
8use core::ffi::c_void;
9
10use alloc::ffi::CString;
11use alloc::format;
12use alloc::string::{String, ToString};
13use alloc::vec::Vec;
14
15use crate::{
16 ErrorKind, LastData, Message, OrtResult, PromptOpts, Response, ThinkEvent, Write,
17 common::config, common::file, common::queue, common::stats, common::utils, ort_from_err,
18};
19use crate::{libc, ort_error};
20
21const CURSOR_ON: &[u8] = "\x1b[?25h".as_bytes();
22
23const MSG_CONNECTING: &[u8] = "\x1b[?25lConnecting...\r".as_bytes();
25
26const MSG_CLEAR_LINE: &[u8] = "\r\x1b[2K\n".as_bytes();
28
29const MSG_PROCESSING: &[u8] = "\x1b[1mProcessing...\x1b[0m\r".as_bytes();
34const MSG_THINK_TAG_END: &[u8] = "\x1b[1m</think>\x1b[0m\n".as_bytes();
35const MSG_THINKING: &[u8] = "\x1b[1mThinking...\x1b[0m ".as_bytes();
36const MSG_THINK_TAG_START: &[u8] = "\x1b[1m<think>\x1b[0m".as_bytes();
37
38const SPINNER: [&[u8]; 4] = [
44 "|\x1b[1D".as_bytes(),
45 "/\x1b[1D".as_bytes(),
46 "-\x1b[1D".as_bytes(),
47 "\\\x1b[1D".as_bytes(),
48];
49
50const ERR_RATE_LIMITED: &str = "429 Too Many Requests";
51
52pub struct ConsoleWriter<W: Write + Send> {
53 pub writer: W, pub show_reasoning: bool,
55}
56
57impl<W: Write + Send> ConsoleWriter<W> {
58 pub fn into_inner(self) -> W {
59 self.writer
60 }
61 pub fn run<const N: usize>(
62 &mut self,
63 mut rx: queue::Consumer<Response, N>,
64 ) -> OrtResult<stats::Stats> {
65 let _ = self.writer.write(MSG_CONNECTING);
66 let _ = self.writer.flush();
67
68 let mut is_first_content = true;
69 let mut spindx = 0;
70 let mut stats_out = None;
71 while let Some(data) = rx.get_next() {
72 match data {
73 Response::Start => {
74 let _ = self.writer.write(MSG_PROCESSING);
75 let _ = self.writer.flush();
76 }
77 Response::Think(think) => {
78 if self.show_reasoning {
79 match think {
80 ThinkEvent::Start => {
81 let _ = self.writer.write(MSG_THINK_TAG_START);
82 }
83 ThinkEvent::Content(s) => {
84 let _ = self.writer.write_all(s.as_bytes());
85 let _ = self.writer.flush();
86 }
87 ThinkEvent::Stop => {
88 let _ = self.writer.write(MSG_THINK_TAG_END);
89 }
90 }
91 } else {
92 match think {
93 ThinkEvent::Start => {
94 let _ = self.writer.write(MSG_THINKING);
95 let _ = self.writer.flush();
96 }
97 ThinkEvent::Content(_) => {
98 let _ = self.writer.write(SPINNER[spindx % SPINNER.len()]);
99 let _ = self.writer.flush();
100 spindx += 1;
101 }
102 ThinkEvent::Stop => {}
103 }
104 }
105 }
106 Response::Content(content) => {
107 if is_first_content {
108 let _ = self.writer.write(MSG_CLEAR_LINE);
110 is_first_content = false;
111 }
112 let _ = self.writer.write_all(content.as_bytes());
113 let _ = self.writer.flush();
114 }
115 Response::Stats(stats) => {
116 stats_out = Some(stats);
117 }
118 Response::Error(err) => {
119 let _ = self.writer.write(CURSOR_ON);
120 let _ = self.writer.flush();
121 if err.to_string().contains(ERR_RATE_LIMITED) {
122 return Err(ort_error(ErrorKind::RateLimited, ""));
123 } else {
124 return Err(ort_from_err(
125 ErrorKind::ResponseStreamError,
126 "OpenRouter said no",
127 err,
128 ));
129 }
130 }
131 Response::None => {
132 panic!("Response::None means we read the wrong Queue position");
133 }
134 }
135 }
136
137 let _ = self.writer.write(CURSOR_ON);
138 let _ = self.writer.flush();
139
140 let Some(stats) = stats_out else {
141 return Err(ort_error(ErrorKind::MissingUsageStats, ""));
142 };
143 Ok(stats)
144 }
145}
146
147pub struct FileWriter<W: Write + Send> {
148 pub writer: W,
149 pub show_reasoning: bool,
150}
151
152impl<W: Write + Send> FileWriter<W> {
153 pub fn into_inner(self) -> W {
154 self.writer
155 }
156 pub fn run<const N: usize>(
157 &mut self,
158 mut rx: queue::Consumer<Response, N>,
159 ) -> OrtResult<stats::Stats> {
160 let mut stats_out = None;
161 while let Some(data) = rx.get_next() {
162 match data {
163 Response::Start => {}
164 Response::Think(think) => {
165 if self.show_reasoning {
166 match think {
167 ThinkEvent::Start => {
168 let _ = self.writer.write("<think>".as_bytes());
169 }
170 ThinkEvent::Content(s) => {
171 let _ = self.writer.write_all(s.as_bytes());
172 }
173 ThinkEvent::Stop => {
174 let _ = self.writer.write("</think>\n\n".as_bytes());
175 }
176 }
177 }
178 }
179 Response::Content(content) => {
180 let _ = self.writer.write_all(content.as_bytes());
181 }
182 Response::Stats(stats) => {
183 stats_out = Some(stats);
184 }
185 Response::Error(err) => {
186 if err.to_string().contains(ERR_RATE_LIMITED) {
187 return Err(ort_error(ErrorKind::RateLimited, ""));
188 } else {
189 return Err(ort_from_err(
190 ErrorKind::ResponseStreamError,
191 "OpenRouter said no",
192 err,
193 ));
194 }
195 }
196 Response::None => {
197 return Err(ort_error(
198 ErrorKind::QueueDesync,
199 "Response::None means we read the wrong Queue position",
200 ));
201 }
202 }
203 }
204
205 let Some(stats) = stats_out else {
206 return Err(ort_error(ErrorKind::MissingUsageStats, ""));
207 };
208 Ok(stats)
209 }
210}
211
212pub struct CollectedWriter {}
213
214impl CollectedWriter {
215 pub fn run<const N: usize>(
216 &mut self,
217 mut rx: queue::Consumer<Response, N>,
218 ) -> OrtResult<String> {
219 let mut got_stats = None;
220 let mut contents = Vec::with_capacity(1024);
221 while let Some(data) = rx.get_next() {
222 match data {
223 Response::Start => {}
224 Response::Think(_) => {}
225 Response::Content(content) => {
226 contents.push(content);
227 }
228 Response::Stats(stats) => {
229 got_stats = Some(stats);
230 }
231 Response::Error(_err) => {
232 return Err(ort_error(
234 ErrorKind::ResponseStreamError,
235 "CollectedWriter response error",
236 ));
237 }
238 Response::None => {
239 return Err(ort_error(
240 ErrorKind::QueueDesync,
241 "Response::None means we read the wrong Queue position",
242 ));
243 }
244 }
245 }
246
247 let out =
248 "--- ".to_string() + &got_stats.unwrap().to_string() + " ---\n" + &contents.join("");
249 Ok(out)
251 }
252}
253
254pub struct LastWriter {
255 w: file::File,
256 data: LastData,
257}
258
259impl LastWriter {
260 pub fn new(opts: PromptOpts, messages: Vec<Message>) -> OrtResult<Self> {
261 let last_filename = format!("last-{}.json", utils::tmux_pane_id());
262 let mut last_path = config::cache_dir()?;
263 last_path.push('/');
264 last_path.push_str(&last_filename);
265 let c_path = CString::new(last_path)
266 .map_err(|e| ort_from_err(ErrorKind::FileCreateFailed, "CString::new last path", e))?;
267 let last_file = unsafe {
268 file::File::create(c_path.as_ptr())
269 .map_err(|e| ort_from_err(ErrorKind::FileCreateFailed, "create last file", e))?
270 };
271 let data = LastData { opts, messages };
272 Ok(LastWriter { data, w: last_file })
273 }
274
275 pub fn run<const N: usize>(
276 &mut self,
277 mut rx: queue::Consumer<Response, N>,
278 ) -> OrtResult<stats::Stats> {
279 let mut contents = Vec::with_capacity(1024);
280 while let Some(data) = rx.get_next() {
281 match data {
282 Response::Start => {}
283 Response::Think(_) => {}
284 Response::Content(content) => {
285 contents.push(content);
286 }
287 Response::Stats(stats) => {
288 self.data.opts.provider = Some(utils::slug(stats.provider()));
289 }
290 Response::Error(_err) => {
291 return Err(ort_error(
293 ErrorKind::LastWriterError,
294 "LastWriter run error",
295 ));
296 }
297 Response::None => {
298 return Err(ort_error(
299 ErrorKind::QueueDesync,
300 "Response::None means we read the wrong Queue position",
301 ));
302 }
303 }
304 }
305
306 let message = Message::assistant(contents.join(""));
307 self.data.messages.push(message);
308
309 self.data.to_json_writer(&mut self.w)?;
310 let _ = (&mut self.w).flush();
311
312 Ok(stats::Stats::default()) }
314}
315
316pub struct StdoutWriter {}
317
318impl Write for StdoutWriter {
319 fn write(&mut self, buf: &[u8]) -> OrtResult<usize> {
320 let bytes_written = unsafe { libc::write(1, buf.as_ptr() as *const c_void, buf.len()) };
321 if bytes_written >= 0 {
322 Ok(bytes_written as usize)
323 } else {
324 Err(ort_error(ErrorKind::StdoutWriteFailed, ""))
325 }
326 }
327
328 fn flush(&mut self) -> OrtResult<()> {
329 Ok(())
330 }
331}