1use std::sync::Arc;
9use std::time::Duration;
10
11use futures::stream::StreamExt;
12
13use crate::circuit_breaker::CircuitBreaker;
14use crate::error::{LlmError, Result};
15use crate::stream::{SseDecoder, StreamAssembler};
16use crate::types::{CompletionRequest, CompletionResponse};
17
18pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
22
23#[derive(Debug, Clone)]
25pub struct LlmConfig {
26 pub base_url: String,
28 pub api_key: String,
30 pub timeout: Duration,
32}
33
34impl LlmConfig {
35 pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Self {
36 Self {
37 base_url: base_url.into(),
38 api_key: api_key.into(),
39 timeout: DEFAULT_TIMEOUT,
40 }
41 }
42}
43
44#[derive(Clone)]
47pub struct LlmClient {
48 http: reqwest::Client,
49 base_url: Arc<str>,
50 api_key: Arc<str>,
51 breaker: Arc<CircuitBreaker>,
52}
53
54impl LlmClient {
55 pub fn new(config: LlmConfig) -> Result<Self> {
57 Self::with_breaker(config, Arc::new(CircuitBreaker::default()))
58 }
59
60 pub fn with_breaker(config: LlmConfig, breaker: Arc<CircuitBreaker>) -> Result<Self> {
63 let http = reqwest::Client::builder().timeout(config.timeout).build()?;
64 Ok(Self {
65 http,
66 base_url: config.base_url.trim_end_matches('/').into(),
67 api_key: config.api_key.into(),
68 breaker,
69 })
70 }
71
72 pub fn breaker(&self) -> &Arc<CircuitBreaker> {
74 &self.breaker
75 }
76
77 pub async fn complete_stream_single_attempt<F>(
80 &self,
81 request: &CompletionRequest,
82 mut on_delta: F,
83 ) -> Result<CompletionResponse>
84 where
85 F: FnMut(&str, bool) + Send,
86 {
87 if self.breaker.is_open() {
88 return Err(LlmError::CircuitOpen);
89 }
90 let url = format!("{}/chat/completions", self.base_url);
91 let result = self.stream_collect_once(&url, request, &mut on_delta).await;
92 match &result {
93 Ok(_) => self.breaker.record_success(),
94 Err(_) => self.breaker.record_failure(),
95 }
96 result
97 }
98
99 async fn stream_collect_once<F>(
100 &self,
101 url: &str,
102 request: &CompletionRequest,
103 on_delta: &mut F,
104 ) -> Result<CompletionResponse>
105 where
106 F: FnMut(&str, bool) + Send,
107 {
108 let mut req = self.http.post(url).json(request);
109 if let Some(attempt_id) = &request.provider_attempt_id {
110 req = req
111 .header("Idempotency-Key", attempt_id)
112 .header("X-Agent-Factory-Attempt-Id", attempt_id);
113 }
114 if !self.api_key.is_empty() {
115 req = req.bearer_auth(self.api_key.as_ref());
116 }
117 let response = req.send().await?;
118 let status = response.status();
119 if !status.is_success() {
120 return Err(LlmError::Api {
121 status: status.as_u16(),
122 body: response.text().await.unwrap_or_default(),
123 });
124 }
125
126 let mut assembler = StreamAssembler::default();
127 let mut decoder = SseDecoder::default();
128 let mut bytes = response.bytes_stream();
129 while let Some(chunk) = bytes.next().await {
130 for data in decoder.push(&chunk?)? {
131 if data == "[DONE]" {
132 decoder.finish()?;
133 return assembler.finish();
134 }
135 if let Some(delta) = assembler.apply_json(&data)? {
136 on_delta(&delta.content, delta.has_tool_calls);
137 }
138 }
139 }
140 decoder.finish()?;
141 Err(LlmError::StreamProtocol(
142 "stream ended before [DONE]".into(),
143 ))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn prepared_streaming_request_is_wire_stable() {
153 let mut request = CompletionRequest::new("model", vec![]).stream(true);
154 request.stream_options = Some(crate::StreamOptions {
155 include_usage: true,
156 });
157 let wire = serde_json::to_value(&request).unwrap();
158 assert_eq!(wire["stream"], true);
159 assert_eq!(wire["stream_options"]["include_usage"], true);
160 }
161}