openai_ng/proto/
file.rs

1use http::Method;
2use reqwest::{
3    multipart::{Form, Part},
4    Body,
5};
6use serde_json::Value;
7use smart_default::SmartDefault;
8use std::{path::PathBuf, time::Duration};
9use tokio_util::codec::{BytesCodec, FramedRead};
10use tracing::*;
11use url::Url;
12
13use crate::{client::Client, error::*};
14
15pub struct FileContentRequest {
16    pub id: String,
17}
18
19impl FileContentRequest {
20    pub fn new(id: impl Into<String>) -> Self {
21        Self { id: id.into() }
22    }
23
24    pub async fn call(
25        &self,
26        client: &Client,
27        timeout: Option<Duration>,
28    ) -> Result<FileContentResponse> {
29        let rep = client
30            .call_impl(
31                Method::GET,
32                &format!("files/{}/content", self.id),
33                vec![],
34                None,
35                None,
36                timeout,
37            )
38            .await?;
39
40        let status = rep.status();
41
42        let rep: Value = serde_json::from_slice(rep.bytes().await?.as_ref())?;
43
44        for l in serde_json::to_string_pretty(&rep)?.lines() {
45            if status.is_success() {
46                trace!(%l, "REP");
47            } else {
48                error!(%l, "REP");
49            }
50        }
51
52        if !status.is_success() {
53            return Err(Error::ApiError(status.as_u16()));
54        }
55
56        let rep: FileContentResponse = serde_json::from_value(rep)?;
57
58        for l in rep.content.lines() {
59            trace!(%l, "REP");
60        }
61
62        Ok(rep)
63    }
64}
65
66#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
67pub struct FileContentResponse {
68    pub file_type: String,
69    pub filename: String,
70    pub title: String,
71    #[serde(rename = "type")]
72    pub typ: String,
73    pub content: String,
74}
75
76pub struct FileDeleteRequest {
77    pub id: String,
78}
79
80impl FileDeleteRequest {
81    pub fn new(id: impl Into<String>) -> Self {
82        Self { id: id.into() }
83    }
84
85    pub async fn call(&self, client: &Client, timeout: Option<Duration>) -> Result<()> {
86        let rep = client
87            .call_impl(
88                Method::DELETE,
89                &format!("files/{}", self.id),
90                vec![],
91                None,
92                None,
93                timeout,
94            )
95            .await?;
96
97        let status = rep.status();
98
99        let rep: Value = serde_json::from_slice(rep.bytes().await?.as_ref())?;
100
101        for l in serde_json::to_string_pretty(&rep)?.lines() {
102            if status.is_success() {
103                trace!(%l, "REP");
104            } else {
105                error!(%l, "REP");
106            }
107        }
108
109        if !status.is_success() {
110            return Err(Error::ApiError(status.as_u16()));
111        }
112
113        Ok(())
114    }
115}
116
117pub struct FileGetRequest {
118    pub id: String,
119}
120
121impl FileGetRequest {
122    pub fn new(id: impl Into<String>) -> Self {
123        Self { id: id.into() }
124    }
125
126    pub async fn call(
127        &self,
128        client: &Client,
129        timeout: Option<Duration>,
130    ) -> Result<FileUploadResponse> {
131        let rep = client
132            .call_impl(
133                Method::GET,
134                &format!("files/{}", self.id),
135                vec![],
136                None,
137                None,
138                timeout,
139            )
140            .await?;
141
142        let status = rep.status();
143
144        let rep: Value = serde_json::from_slice(rep.bytes().await?.as_ref())?;
145
146        for l in serde_json::to_string_pretty(&rep)?.lines() {
147            if status.is_success() {
148                trace!(%l, "REP");
149            } else {
150                error!(%l, "REP");
151                return Err(Error::ApiError(status.as_u16()));
152            }
153        }
154
155        Ok(serde_json::from_value(rep)?)
156    }
157}
158
159pub struct FileListRequest;
160
161impl FileListRequest {
162    pub async fn call(
163        &self,
164        client: &Client,
165        timeout: Option<Duration>,
166    ) -> Result<FileListResponse> {
167        let rep = client
168            .call_impl(Method::GET, "files", vec![], None, None, timeout)
169            .await?;
170
171        let status = rep.status();
172
173        let rep: Value = serde_json::from_slice(rep.bytes().await?.as_ref())?;
174
175        for l in serde_json::to_string_pretty(&rep)?.lines() {
176            if status.is_success() {
177                trace!(%l, "REP");
178            } else {
179                error!(%l, "REP");
180                return Err(Error::ApiError(status.as_u16()));
181            }
182        }
183
184        Ok(serde_json::from_value(rep)?)
185    }
186}
187
188#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
189pub struct FileListResponse {
190    pub object: String,
191    pub data: Vec<FileUploadResponse>,
192}
193
194impl From<PathBuf> for FileSource {
195    fn from(value: PathBuf) -> Self {
196        Self::Local(value)
197    }
198}
199
200impl From<Url> for FileSource {
201    fn from(value: Url) -> Self {
202        Self::Remote {
203            url: value,
204            trust_all_certification: true,
205        }
206    }
207}
208
209pub enum FileSource {
210    Local(PathBuf),
211    Remote {
212        url: Url,
213        trust_all_certification: bool,
214    },
215}
216
217#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SmartDefault)]
218pub enum FilePurpose {
219    #[default]
220    #[serde(rename = "file-extract")]
221    Extract,
222}
223
224impl From<FilePurpose> for String {
225    fn from(value: FilePurpose) -> Self {
226        match value {
227            FilePurpose::Extract => "file-extract".to_string(),
228        }
229    }
230}
231
232impl From<&FilePurpose> for String {
233    fn from(value: &FilePurpose) -> Self {
234        match value {
235            FilePurpose::Extract => "file-extract".to_string(),
236        }
237    }
238}
239
240pub struct FileUploadRequest {
241    pub source: FileSource,
242    pub purpose: FilePurpose,
243}
244
245impl FileUploadRequest {
246    pub async fn call(
247        &self,
248        client: &Client,
249        timeout: Option<Duration>,
250    ) -> Result<FileUploadResponse> {
251        let part = match &self.source {
252            FileSource::Local(local_path) => {
253                let file_name = local_path
254                    .file_name()
255                    .and_then(|s| s.to_str())
256                    .map(|s| s.to_string())
257                    .ok_or(Error::NoFileName)?;
258                let file = tokio::fs::File::open(local_path).await?;
259                let stream = FramedRead::new(file, BytesCodec::new());
260                let file_body = Body::wrap_stream(stream);
261                Part::stream(file_body).file_name(file_name)
262            }
263            FileSource::Remote {
264                url,
265                trust_all_certification,
266            } => {
267                let filename = PathBuf::from(url.path())
268                    .file_name()
269                    .and_then(|s| s.to_str())
270                    .map(|s| s.to_string())
271                    .ok_or(Error::NoFileName)?;
272
273                trace!(%trust_all_certification, %filename, "upload remote url={}", url.as_str());
274
275                let rep = reqwest::Client::builder()
276                    .danger_accept_invalid_certs(*trust_all_certification)
277                    .build()?
278                    .get(url.clone())
279                    .send()
280                    .await?;
281
282                let bytes = rep.bytes().await?;
283                Part::stream(bytes).file_name(filename)
284            }
285        };
286
287        let purpose = String::from(&self.purpose);
288
289        info!(?purpose);
290
291        let form = Form::new()
292            .text("purpose", String::from(&self.purpose))
293            .part("file", part);
294
295        let rep = client
296            .call_impl(Method::POST, "files", vec![], None, Some(form), timeout)
297            .await?;
298
299        let status = rep.status();
300
301        let rep: Value = serde_json::from_slice(rep.bytes().await?.as_ref())?;
302
303        for l in serde_json::to_string_pretty(&rep)?.lines() {
304            if status.is_success() {
305                trace!(%l, "REP");
306            } else {
307                error!(%l, "REP");
308                return Err(Error::ApiError(status.as_u16()));
309            }
310        }
311
312        Ok(serde_json::from_value(rep)?)
313    }
314}
315
316impl FileUploadRequest {
317    pub fn builder() -> FileUploadRequestBuilder {
318        FileUploadRequestBuilder::default()
319    }
320}
321
322#[derive(SmartDefault)]
323pub struct FileUploadRequestBuilder {
324    source: Option<FileSource>,
325    purpose: FilePurpose,
326}
327
328impl FileUploadRequestBuilder {
329    pub fn with_source(mut self, source: impl Into<FileSource>) -> Self {
330        self.source = Some(source.into());
331        self
332    }
333
334    pub fn with_purpose(mut self, purpose: FilePurpose) -> Self {
335        self.purpose = purpose;
336        self
337    }
338
339    pub fn build(self) -> Result<FileUploadRequest> {
340        Ok(FileUploadRequest {
341            source: self.source.ok_or(Error::FileRequestBuild)?,
342            purpose: self.purpose,
343        })
344    }
345}
346
347#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
348pub struct FileUploadResponse {
349    pub id: String,
350    pub object: String,
351    pub bytes: usize,
352    pub created_at: u64,
353    pub filename: String,
354    pub purpose: FilePurpose,
355    pub status: String,
356    pub status_details: String,
357}
358
359#[cfg(test)]
360#[tokio::test]
361async fn test_file_upload_ok() -> anyhow::Result<()> {
362    use crate::auth::Bearer;
363    let _ = dotenv::from_filename(".env.kimi");
364
365    let _ = tracing_subscriber::fmt::try_init();
366    let base_url = std::env::var("OPENAI_API_BASE_URL")?;
367    let key = std::env::var("OPENAI_API_KEY")?;
368    let version = std::env::var("OPENAI_API_VERSION")?;
369    let model_name = std::env::var("OPENAI_API_MODEL_NAME")?;
370    let vision_available = std::env::var("OPENAI_API_VISION").is_ok();
371    let use_stream = std::env::var("USE_STREAM").is_ok();
372
373    info!(%base_url, %key, %version, %model_name, %vision_available, %use_stream, "start test with");
374
375    let client = Client::builder()
376        .with_authenticator(Bearer::new(key))?
377        .with_base_url(base_url)?
378        .with_version(version)?
379        .build()?;
380
381    let rep = FileListRequest.call(&client, None).await?;
382
383    for item in &rep.data {
384        if item.filename != "161528_24 司马光 优质教案.pdf" {
385            continue;
386        }
387        let rep = FileGetRequest::new(&item.id).call(&client, None).await?;
388        let rep = FileContentRequest::new(&item.id)
389            .call(&client, None)
390            .await?;
391    }
392
393    // let source =
394    //     PathBuf::from("/Users/yexiangyu/Repo/openai-ng/tests/161528_24 司马光 优质教案.pdf");
395
396    // // let source = Url::parse("https://changkun.de/modern-cpp/pdf/modern-cpp-tutorial-en-us.pdf")?;
397
398    // let req = FileUploadRequest::builder()
399    //     .with_source(source)
400    //     .with_purpose(FilePurpose::Extract)
401    //     .build()?;
402
403    // let rep = req.call(&client, None).await?;
404
405    Ok(())
406}