agentic_core/executor/
inference.rs1use std::sync::Arc;
7use std::time::Duration;
8
9use async_stream::stream;
10use futures::{Stream, StreamExt};
11
12use crate::executor::error::{ExecutorError, ExecutorResult};
13
14pub type BoxStream = std::pin::Pin<Box<dyn Stream<Item = String> + Send>>;
16
17pub(super) const DONE_MARKER: &str = "data: [DONE]\n\n";
19
20pub(super) async fn next_chunk<S>(stream: &mut S, timeout: Duration) -> ExecutorResult<Option<bytes::Bytes>>
25where
26 S: futures::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Unpin,
27{
28 let item = if timeout.is_zero() {
29 stream.next().await
30 } else {
31 tokio::time::timeout(timeout, stream.next()).await.map_err(|_| {
32 ExecutorError::StreamError("chunk timeout: no data received within the configured window".into())
33 })?
34 };
35 item.transpose().map_err(ExecutorError::NetworkError)
36}
37
38fn drain_complete_utf8_lines(buffer: &mut Vec<u8>) -> Vec<String> {
39 let mut lines = Vec::new();
40 while let Some(pos) = buffer.iter().position(|byte| *byte == b'\n') {
41 let line = buffer.drain(..=pos).collect::<Vec<_>>();
42 let line_end = if pos > 0 && line.get(pos - 1) == Some(&b'\r') {
43 pos - 1
44 } else {
45 pos
46 };
47 if let Ok(line) = std::str::from_utf8(&line[..line_end]) {
48 lines.push(line.to_string());
49 }
50 }
51 lines
52}
53
54pub(super) async fn send_request(
60 client: &reqwest::Client,
61 url: &str,
62 body: String,
63 auth: Option<&str>,
64) -> ExecutorResult<reqwest::Response> {
65 let mut req = client.post(url).header("Content-Type", "application/json").body(body);
66 if let Some(key) = auth {
67 req = req.bearer_auth(key);
68 }
69
70 let resp = req.send().await.map_err(|e| ExecutorError::LLMRequest {
71 status: if e.is_timeout() {
72 http::StatusCode::GATEWAY_TIMEOUT
73 } else {
74 http::StatusCode::BAD_GATEWAY
75 },
76 body: if e.is_timeout() {
77 "upstream timeout".into()
78 } else {
79 "upstream unavailable".into()
80 },
81 })?;
82
83 if !resp.status().is_success() {
84 let status = resp.status().as_u16();
85 let body = resp
88 .text()
89 .await
90 .inspect_err(|e| tracing::debug!("failed to read error response body: {e}"))
91 .unwrap_or_default();
92 return Err(ExecutorError::LLMRequest {
93 status: http::StatusCode::from_u16(status).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
94 body,
95 });
96 }
97
98 Ok(resp)
99}
100
101pub(super) async fn fetch_response_json(
105 upstream_json: String,
106 url: &str,
107 client: &reqwest::Client,
108 auth: Option<&str>,
109) -> ExecutorResult<String> {
110 let resp = send_request(client, url, upstream_json, auth).await?;
111 resp.text().await.map_err(ExecutorError::NetworkError)
113}
114
115pub fn call_inference(
125 upstream_json: String,
126 url: String,
127 client: Arc<reqwest::Client>,
128 auth: Option<String>,
129 chunk_timeout: Duration,
130) -> impl Stream<Item = Result<String, ExecutorError>> + Send + 'static {
131 stream! {
132 let resp = match send_request(&client, &url, upstream_json, auth.as_deref()).await {
133 Ok(r) => r,
134 Err(e) => { yield Err(e); return; }
135 };
136
137 let mut bytes = resp.bytes_stream();
138 let mut buf = Vec::with_capacity(8192);
139
140 loop {
141 let chunk = match next_chunk(&mut bytes, chunk_timeout).await {
142 Ok(Some(c)) => c,
143 Ok(None) => break,
144 Err(e) => { yield Err(e); return; }
145 };
146
147 buf.extend_from_slice(&chunk);
148
149 for line in drain_complete_utf8_lines(&mut buf) {
150 match line.as_str() {
151 "data: [DONE]" => return,
152 l if l.starts_with("data: ") => yield Ok(line),
153 _ => {}
154 }
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn utf8_line_reader_preserves_split_multibyte_characters() {
166 let snowman = "\u{2603}";
167 let line = format!(r#"data: {{"delta":"snow {snowman}"}}"#);
168 let bytes = format!("{line}\n").into_bytes();
169 let split_at = bytes
170 .windows(snowman.len())
171 .position(|window| window == snowman.as_bytes())
172 .expect("snowman bytes present")
173 + 1;
174 let mut buffer = bytes[..split_at].to_vec();
175
176 assert!(drain_complete_utf8_lines(&mut buffer).is_empty());
177
178 buffer.extend_from_slice(&bytes[split_at..]);
179 let lines = drain_complete_utf8_lines(&mut buffer);
180
181 assert!(buffer.is_empty());
182 assert_eq!(lines, vec![line]);
183 assert!(!lines[0].contains('\u{FFFD}'));
184 }
185}