Skip to main content

buble/
chat.rs

1use futures_util::StreamExt;
2use reqwest::Method;
3use serde_json::Value;
4
5use crate::{
6    client::Client,
7    error::Result,
8    http::encode_model_path,
9    request::set_stream,
10    streaming::{events_from_bytes, text_from_event, EventStream, StreamProtocol, TextStream},
11    ChatModelList,
12};
13
14/// Flexible chat request body.
15pub type ChatRequest = Value;
16
17/// Protocol-native chat response body.
18pub type ChatResponse = Value;
19
20/// Chat model methods for OpenAI, Anthropic, and Gemini-compatible APIs.
21#[derive(Clone, Debug)]
22pub struct ChatService {
23    client: Client,
24}
25
26impl ChatService {
27    pub(crate) fn new(client: Client) -> Self {
28        Self { client }
29    }
30
31    /// Chat model discovery methods.
32    pub fn models(&self) -> ChatModelsService {
33        ChatModelsService::new(self.client.clone())
34    }
35
36    /// OpenAI-compatible chat completions methods.
37    pub fn completions(&self) -> ChatCompletionsService {
38        ChatCompletionsService::new(self.client.clone())
39    }
40
41    /// Anthropic Messages-compatible methods.
42    pub fn messages(&self) -> MessagesService {
43        MessagesService::new(self.client.clone())
44    }
45
46    /// Gemini-compatible methods.
47    pub fn gemini(&self) -> GeminiService {
48        GeminiService::new(self.client.clone())
49    }
50}
51
52/// Chat model discovery methods.
53#[derive(Clone, Debug)]
54pub struct ChatModelsService {
55    client: Client,
56}
57
58impl ChatModelsService {
59    pub(crate) fn new(client: Client) -> Self {
60        Self { client }
61    }
62
63    /// Lists active chat models.
64    pub async fn list(&self) -> Result<ChatModelList> {
65        self.client
66            .request_json::<ChatModelList, ()>(Method::GET, "/api/v1/models", None, None)
67            .await
68    }
69}
70
71/// OpenAI-compatible chat completion methods.
72#[derive(Clone, Debug)]
73pub struct ChatCompletionsService {
74    client: Client,
75}
76
77impl ChatCompletionsService {
78    pub(crate) fn new(client: Client) -> Self {
79        Self { client }
80    }
81
82    /// Calls the OpenAI-compatible chat completions endpoint.
83    pub async fn create(&self, mut body: ChatRequest) -> Result<ChatResponse> {
84        set_stream(&mut body, false);
85        self.client
86            .request_value(Method::POST, "/api/v1/chat/completions", None, Some(&body))
87            .await
88    }
89
90    /// Streams raw OpenAI-compatible server-sent events.
91    pub async fn stream(&self, mut body: ChatRequest) -> Result<EventStream> {
92        set_stream(&mut body, true);
93        let bytes = self
94            .client
95            .stream_json(Method::POST, "/api/v1/chat/completions", Some(&body))
96            .await?;
97        Ok(events_from_bytes(bytes))
98    }
99
100    /// Streams extracted OpenAI-compatible text deltas.
101    pub async fn stream_text(&self, body: ChatRequest) -> Result<TextStream> {
102        let stream = self.stream(body).await?;
103        Ok(Box::pin(stream.filter_map(|event| async move {
104            match event {
105                Ok(event) => {
106                    let text = text_from_event(&event, StreamProtocol::OpenAi);
107                    if text.is_empty() {
108                        None
109                    } else {
110                        Some(Ok(text))
111                    }
112                }
113                Err(error) => Some(Err(error)),
114            }
115        })))
116    }
117}
118
119/// Anthropic Messages-compatible methods.
120#[derive(Clone, Debug)]
121pub struct MessagesService {
122    client: Client,
123}
124
125impl MessagesService {
126    pub(crate) fn new(client: Client) -> Self {
127        Self { client }
128    }
129
130    /// Calls the Anthropic Messages-compatible endpoint.
131    pub async fn create(&self, mut body: ChatRequest) -> Result<ChatResponse> {
132        set_stream(&mut body, false);
133        self.client
134            .request_value(Method::POST, "/api/v1/messages", None, Some(&body))
135            .await
136    }
137
138    /// Streams raw Anthropic-compatible server-sent events.
139    pub async fn stream(&self, mut body: ChatRequest) -> Result<EventStream> {
140        set_stream(&mut body, true);
141        let bytes = self
142            .client
143            .stream_json(Method::POST, "/api/v1/messages", Some(&body))
144            .await?;
145        Ok(events_from_bytes(bytes))
146    }
147
148    /// Streams extracted Anthropic-compatible text deltas.
149    pub async fn stream_text(&self, body: ChatRequest) -> Result<TextStream> {
150        let stream = self.stream(body).await?;
151        Ok(Box::pin(stream.filter_map(|event| async move {
152            match event {
153                Ok(event) => {
154                    let text = text_from_event(&event, StreamProtocol::Anthropic);
155                    if text.is_empty() {
156                        None
157                    } else {
158                        Some(Ok(text))
159                    }
160                }
161                Err(error) => Some(Err(error)),
162            }
163        })))
164    }
165}
166
167/// Gemini-compatible content generation methods.
168#[derive(Clone, Debug)]
169pub struct GeminiService {
170    client: Client,
171}
172
173impl GeminiService {
174    pub(crate) fn new(client: Client) -> Self {
175        Self { client }
176    }
177
178    /// Calls the Gemini-compatible non-streaming endpoint.
179    pub async fn generate_content(&self, model: &str, body: ChatRequest) -> Result<ChatResponse> {
180        let path = format!(
181            "/api/v1beta/models/{}:generateContent",
182            encode_model_path(model)
183        );
184        self.client
185            .request_value(Method::POST, &path, None, Some(&body))
186            .await
187    }
188
189    /// Streams raw Gemini-compatible server-sent events.
190    pub async fn stream_generate_content(
191        &self,
192        model: &str,
193        body: ChatRequest,
194    ) -> Result<EventStream> {
195        let path = format!(
196            "/api/v1beta/models/{}:streamGenerateContent",
197            encode_model_path(model)
198        );
199        let bytes = self
200            .client
201            .stream_json(Method::POST, &path, Some(&body))
202            .await?;
203        Ok(events_from_bytes(bytes))
204    }
205
206    /// Streams extracted Gemini-compatible text chunks.
207    pub async fn stream_text(&self, model: &str, body: ChatRequest) -> Result<TextStream> {
208        let stream = self.stream_generate_content(model, body).await?;
209        Ok(Box::pin(stream.filter_map(|event| async move {
210            match event {
211                Ok(event) => {
212                    let text = text_from_event(&event, StreamProtocol::Gemini);
213                    if text.is_empty() {
214                        None
215                    } else {
216                        Some(Ok(text))
217                    }
218                }
219                Err(error) => Some(Err(error)),
220            }
221        })))
222    }
223}