1use std::collections::VecDeque;
13use std::sync::{Arc, Mutex};
14
15use futures_util::stream::BoxStream;
16use hotl_types::{Item, StopReason, TokenUsage};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolDef {
22 pub name: String,
23 pub description: String,
24 pub input_schema: Value,
25}
26
27#[derive(Debug, Clone)]
28pub struct SamplingRequest {
29 pub model: String,
30 pub max_tokens: u32,
31 pub system: Arc<str>,
33 pub items: Arc<Vec<Item>>,
34 pub tools: Arc<[ToolDef]>,
35 pub thinking: bool,
37 pub cache_static: bool,
40 pub turn_context: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(tag = "event", rename_all = "snake_case")]
48pub enum StreamEvent {
49 Started,
50 BlockStart {
51 index: usize,
52 kind: String,
53 },
54 TextDelta {
55 index: usize,
56 text: String,
57 },
58 ThinkingDelta {
59 index: usize,
60 text: String,
61 },
62 ToolInputDelta {
63 index: usize,
64 json: String,
65 },
66 BlockEnd {
67 index: usize,
68 },
69 Retrying {
70 attempt: u32,
71 reason: String,
72 },
73 Completed {
76 stop: StopReason,
77 usage: TokenUsage,
78 blocks: Vec<Value>,
79 },
80}
81
82#[derive(Debug, thiserror::Error)]
83pub enum ProviderError {
84 #[error("authentication failed: {0}")]
85 Auth(String),
86 #[error("HTTP {status}: {message}")]
87 Http {
88 status: u16,
89 message: String,
90 retry_after: Option<u64>,
91 },
92 #[error("transport error: {0}")]
93 Transport(String),
94 #[error("stream parse error: {0}")]
95 Parse(String),
96}
97
98pub trait Provider: Send + Sync {
99 fn stream(
100 &self,
101 req: SamplingRequest,
102 ) -> BoxStream<'static, Result<StreamEvent, ProviderError>>;
103}
104
105pub struct ScriptedProvider {
108 scripts: Mutex<VecDeque<Vec<Result<StreamEvent, ProviderError>>>>,
109 requests: Mutex<Vec<SamplingRequest>>,
111}
112
113impl ScriptedProvider {
114 pub fn new(scripts: Vec<Vec<Result<StreamEvent, ProviderError>>>) -> Self {
115 Self {
116 scripts: Mutex::new(scripts.into()),
117 requests: Mutex::new(Vec::new()),
118 }
119 }
120
121 pub fn requests(&self) -> Vec<SamplingRequest> {
124 self.requests.lock().expect("requests mutex").clone()
125 }
126
127 pub fn last_request(&self) -> Option<SamplingRequest> {
129 self.requests
130 .lock()
131 .expect("requests mutex")
132 .last()
133 .cloned()
134 }
135
136 pub fn request_count(&self) -> usize {
137 self.requests.lock().expect("requests mutex").len()
138 }
139
140 pub fn push_script(&self, script: Vec<Result<StreamEvent, ProviderError>>) {
143 self.scripts
144 .lock()
145 .expect("scripted provider mutex")
146 .push_back(script);
147 }
148
149 pub fn text_reply(text: &str) -> Vec<Result<StreamEvent, ProviderError>> {
151 vec![
152 Ok(StreamEvent::Started),
153 Ok(StreamEvent::BlockStart {
154 index: 0,
155 kind: "text".into(),
156 }),
157 Ok(StreamEvent::TextDelta {
158 index: 0,
159 text: text.into(),
160 }),
161 Ok(StreamEvent::BlockEnd { index: 0 }),
162 Ok(StreamEvent::Completed {
163 stop: StopReason::EndTurn,
164 usage: TokenUsage {
165 input_tokens: 10,
166 output_tokens: 5,
167 ..Default::default()
168 },
169 blocks: vec![serde_json::json!({"type": "text", "text": text})],
170 }),
171 ]
172 }
173
174 pub fn tool_call(
176 id: &str,
177 name: &str,
178 input: Value,
179 ) -> Vec<Result<StreamEvent, ProviderError>> {
180 let block = serde_json::json!({"type": "tool_use", "id": id, "name": name, "input": input});
181 vec![
182 Ok(StreamEvent::Started),
183 Ok(StreamEvent::BlockStart {
184 index: 0,
185 kind: "tool_use".into(),
186 }),
187 Ok(StreamEvent::BlockEnd { index: 0 }),
188 Ok(StreamEvent::Completed {
189 stop: StopReason::ToolUse,
190 usage: TokenUsage {
191 input_tokens: 10,
192 output_tokens: 8,
193 ..Default::default()
194 },
195 blocks: vec![block],
196 }),
197 ]
198 }
199}
200
201impl Provider for ScriptedProvider {
202 fn stream(
203 &self,
204 req: SamplingRequest,
205 ) -> BoxStream<'static, Result<StreamEvent, ProviderError>> {
206 self.requests.lock().expect("requests mutex").push(req);
207 let script = self
208 .scripts
209 .lock()
210 .expect("scripted provider mutex")
211 .pop_front()
212 .unwrap_or_else(|| {
213 vec![Err(ProviderError::Transport(
214 "scripted provider exhausted".into(),
215 ))]
216 });
217 Box::pin(futures_util::stream::iter(script))
218 }
219}
220
221#[derive(Default)]
225pub struct SseParser {
226 buf: Vec<u8>,
227}
228
229const SSE_MAX_BUFFER: usize = 1024 * 1024;
231
232impl SseParser {
233 pub fn feed(&mut self, chunk: &[u8]) -> Result<Vec<String>, ProviderError> {
236 self.buf.extend_from_slice(chunk);
237 let mut out = Vec::new();
238 let mut start = 0;
239 while let Some(pos) = self.buf[start..].iter().position(|&b| b == b'\n') {
240 let line = String::from_utf8_lossy(&self.buf[start..start + pos]);
241 let line = line.trim_end_matches('\r');
242 if let Some(data) = line.strip_prefix("data:") {
243 let data = data.trim_start();
244 if !data.is_empty() && data != "[DONE]" {
245 out.push(data.to_string());
246 }
247 }
248 start += pos + 1;
249 }
250 self.buf.drain(..start);
251 if self.buf.len() > SSE_MAX_BUFFER {
252 return Err(ProviderError::Parse(format!(
253 "SSE line exceeded {SSE_MAX_BUFFER} bytes without a newline"
254 )));
255 }
256 Ok(out)
257 }
258}
259
260pub mod retry {
263 use super::ProviderError;
264
265 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
266 pub enum Decision {
267 Retry { after_secs: u64 },
269 Fatal,
271 }
272
273 pub const MAX_ATTEMPTS: u32 = 3;
274
275 pub fn classify(err: &ProviderError, attempt: u32) -> Decision {
277 if attempt >= MAX_ATTEMPTS {
278 return Decision::Fatal;
279 }
280 match err {
281 ProviderError::Http {
282 status,
283 retry_after,
284 ..
285 } if *status == 429 || *status >= 500 => Decision::Retry {
286 after_secs: retry_after.unwrap_or(1u64 << (attempt - 1)),
287 },
288 ProviderError::Transport(_) => Decision::Retry {
289 after_secs: 1u64 << (attempt - 1),
290 },
291 _ => Decision::Fatal,
292 }
293 }
294
295 pub fn is_availability(err: &ProviderError) -> bool {
298 matches!(
299 err,
300 ProviderError::Http { status, .. } if *status == 429 || *status >= 500
301 ) || matches!(err, ProviderError::Transport(_))
302 }
303
304 pub fn is_context_overflow(err: &ProviderError) -> bool {
310 let ProviderError::Http {
311 status: 400,
312 message,
313 ..
314 } = err
315 else {
316 return false;
317 };
318 let m = message.to_lowercase();
319 [
320 "too long",
321 "context length",
322 "context window",
323 "tokens exceed",
324 ]
325 .iter()
326 .any(|needle| m.contains(needle))
327 }
328
329 #[cfg(test)]
330 mod tests {
331 use super::*;
332
333 #[test]
334 fn overflow_detection() {
335 let overflow = ProviderError::Http {
336 status: 400,
337 message:
338 r#"{"error":{"message":"prompt is too long: 210000 tokens > 200000 maximum"}}"#
339 .into(),
340 retry_after: None,
341 };
342 assert!(is_context_overflow(&overflow));
343 let oai = ProviderError::Http {
344 status: 400,
345 message: "This model's maximum context length is 128000 tokens".into(),
346 retry_after: None,
347 };
348 assert!(is_context_overflow(&oai));
349 let plain_400 = ProviderError::Http {
350 status: 400,
351 message: "bad schema".into(),
352 retry_after: None,
353 };
354 assert!(!is_context_overflow(&plain_400));
355 }
356
357 #[test]
358 fn classify_rules() {
359 let overload = ProviderError::Http {
360 status: 529,
361 message: String::new(),
362 retry_after: Some(7),
363 };
364 assert_eq!(classify(&overload, 1), Decision::Retry { after_secs: 7 });
365 assert_eq!(classify(&overload, MAX_ATTEMPTS), Decision::Fatal);
366 let auth = ProviderError::Auth("bad".into());
367 assert_eq!(classify(&auth, 1), Decision::Fatal);
368 assert!(!is_availability(&auth));
369 let transport = ProviderError::Transport("reset".into());
370 assert_eq!(classify(&transport, 2), Decision::Retry { after_secs: 2 });
371 assert!(is_availability(&transport));
372 let bad_req = ProviderError::Http {
373 status: 400,
374 message: String::new(),
375 retry_after: None,
376 };
377 assert_eq!(classify(&bad_req, 1), Decision::Fatal);
378 }
379 }
380}
381
382pub mod transform {
387 use serde_json::Value;
388
389 pub fn strip_foreign_reasoning(blocks: &[Value]) -> Vec<Value> {
392 blocks
393 .iter()
394 .filter(|b| {
395 !matches!(
396 b.get("type").and_then(Value::as_str),
397 Some("thinking") | Some("redacted_thinking")
398 )
399 })
400 .cloned()
401 .collect()
402 }
403
404 #[cfg(test)]
405 mod tests {
406 use super::*;
407 use serde_json::json;
408
409 #[test]
410 fn strips_thinking_keeps_rest() {
411 let blocks = vec![
412 json!({"type":"thinking","thinking":"x","signature":"s"}),
413 json!({"type":"redacted_thinking","data":"d"}),
414 json!({"type":"text","text":"hi"}),
415 json!({"type":"tool_use","id":"1","name":"read","input":{}}),
416 ];
417 let out = strip_foreign_reasoning(&blocks);
418 assert_eq!(out.len(), 2);
419 assert_eq!(out[0]["type"], "text");
420 assert_eq!(out[1]["type"], "tool_use");
421 }
422 }
423}
424
425pub mod repair {
430 use serde_json::Value;
431
432 pub fn parse_or_repair(raw: &str) -> Option<Value> {
435 if let Ok(v) = serde_json::from_str(raw) {
436 return Some(v);
437 }
438 let without_commas = strip_trailing_commas(raw);
439 if let Ok(v) = serde_json::from_str(&without_commas) {
440 return Some(v);
441 }
442 serde_json::from_str(&close_truncation(&without_commas)).ok()
443 }
444
445 fn strip_trailing_commas(s: &str) -> String {
447 let mut out = String::with_capacity(s.len());
448 let mut in_string = false;
449 let mut escaped = false;
450 for c in s.chars() {
451 if in_string {
452 out.push(c);
453 if escaped {
454 escaped = false;
455 } else if c == '\\' {
456 escaped = true;
457 } else if c == '"' {
458 in_string = false;
459 }
460 continue;
461 }
462 match c {
463 '"' => {
464 in_string = true;
465 out.push(c);
466 }
467 '}' | ']' => {
468 while out.ends_with(char::is_whitespace) || out.ends_with(',') {
469 if out.ends_with(',') {
470 out.pop();
471 break;
472 }
473 out.pop();
474 }
475 out.push(c);
476 }
477 _ => out.push(c),
478 }
479 }
480 out
481 }
482
483 fn close_truncation(s: &str) -> String {
485 let mut stack = Vec::new();
486 let mut in_string = false;
487 let mut escaped = false;
488 for c in s.chars() {
489 if in_string {
490 if escaped {
491 escaped = false;
492 } else if c == '\\' {
493 escaped = true;
494 } else if c == '"' {
495 in_string = false;
496 }
497 continue;
498 }
499 match c {
500 '"' => in_string = true,
501 '{' => stack.push('}'),
502 '[' => stack.push(']'),
503 '}' | ']' => {
504 stack.pop();
505 }
506 _ => {}
507 }
508 }
509 let mut out = s.to_string();
510 if in_string {
511 out.push('"');
512 }
513 while let Some(closer) = stack.pop() {
514 out.push(closer);
515 }
516 out
517 }
518
519 #[cfg(test)]
520 mod tests {
521 use super::*;
522
523 #[test]
524 fn repairs_common_damage_and_rejects_garbage() {
525 assert_eq!(
526 parse_or_repair(r#"{"path": "a.rs"}"#).unwrap()["path"],
527 "a.rs"
528 );
529 assert_eq!(
530 parse_or_repair(r#"{"path": "a.rs",}"#).unwrap()["path"],
531 "a.rs"
532 );
533 assert_eq!(
534 parse_or_repair(r#"{"items": [1, 2,]}"#).unwrap()["items"][1],
535 2
536 );
537 let v = parse_or_repair(r#"{"command": "cargo tes"#).unwrap();
539 assert_eq!(v["command"], "cargo tes");
540 assert_eq!(parse_or_repair(r#"{"t": "a,}"}"#).unwrap()["t"], "a,}");
542 let v = parse_or_repair(r#"{"t": "say \"hi\"",}"#).unwrap();
544 assert_eq!(v["t"], "say \"hi\"");
545 assert!(parse_or_repair("not json at all").is_none());
546 }
547 }
548}
549
550pub mod key;
551
552pub trait SseAssembler {
555 fn handle(&mut self, data: &str) -> Result<Vec<StreamEvent>, ProviderError>;
556 fn finish(self) -> Result<StreamEvent, ProviderError>;
557}
558
559pub fn drive_sse<B, E, A>(
562 bytes: B,
563 mut assembler: A,
564) -> impl futures_util::Stream<Item = Result<StreamEvent, ProviderError>>
565where
566 B: futures_util::Stream<Item = Result<bytes::Bytes, E>>,
567 E: std::fmt::Display,
568 A: SseAssembler,
569{
570 async_stream::stream! {
571 let mut parser = SseParser::default();
572 futures_util::pin_mut!(bytes);
573 use futures_util::StreamExt;
574 while let Some(chunk) = bytes.next().await {
575 let chunk = match chunk {
576 Ok(c) => c,
577 Err(e) => {
578 yield Err(ProviderError::Transport(format!("stream interrupted: {e}")));
579 return;
580 }
581 };
582 let payloads = match parser.feed(&chunk) {
583 Ok(payloads) => payloads,
584 Err(e) => { yield Err(e); return; }
585 };
586 for data in payloads {
587 match assembler.handle(&data) {
588 Ok(events) => for ev in events { yield Ok(ev); },
589 Err(e) => { yield Err(e); return; }
590 }
591 }
592 }
593 yield assembler.finish();
594 }
595}