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 Ok(())
406}