rig_core/providers/openai/
transcription.rs1use bytes::Bytes;
2
3use crate::http_client::multipart::Part;
4use crate::http_client::{HttpClientExt, MultipartForm};
5use crate::providers::openai::{Client, client::ApiResponse};
6use crate::transcription;
7use crate::transcription::TranscriptionError;
8use serde::Deserialize;
9
10pub const WHISPER_1: &str = "whisper-1";
15
16#[derive(Debug, Deserialize)]
17pub struct TranscriptionResponse {
18 pub text: String,
19}
20
21impl TryFrom<TranscriptionResponse>
22 for transcription::TranscriptionResponse<TranscriptionResponse>
23{
24 type Error = TranscriptionError;
25
26 fn try_from(value: TranscriptionResponse) -> Result<Self, Self::Error> {
27 Ok(transcription::TranscriptionResponse {
28 text: value.text.clone(),
29 response: value,
30 })
31 }
32}
33
34#[derive(Clone)]
35pub struct TranscriptionModel<T = reqwest::Client> {
36 client: Client<T>,
37 pub model: String,
38}
39
40impl<T> TranscriptionModel<T> {
41 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
42 Self {
43 client,
44 model: model.into(),
45 }
46 }
47}
48
49impl<T> transcription::TranscriptionModel for TranscriptionModel<T>
50where
51 T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
52{
53 type Response = TranscriptionResponse;
54
55 type Client = Client<T>;
56
57 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
58 Self::new(client.clone(), model)
59 }
60
61 async fn transcription(
62 &self,
63 request: transcription::TranscriptionRequest,
64 ) -> Result<
65 transcription::TranscriptionResponse<Self::Response>,
66 transcription::TranscriptionError,
67 > {
68 let data = request.data;
69
70 let mut body = MultipartForm::new()
71 .text("model", self.model.clone())
72 .part(Part::bytes("file", data).filename(request.filename.clone()));
73
74 if let Some(language) = request.language {
75 body = body.text("language", language);
76 }
77
78 if let Some(prompt) = request.prompt {
79 body = body.text("prompt", prompt.clone());
80 }
81
82 if let Some(ref temperature) = request.temperature {
83 body = body.text("temperature", temperature.to_string());
84 }
85
86 if let Some(ref additional_params) = request.additional_params {
87 let params = additional_params.as_object().ok_or_else(|| {
88 TranscriptionError::RequestError(Box::new(std::io::Error::new(
89 std::io::ErrorKind::InvalidInput,
90 "additional transcription parameters must be a JSON object",
91 )))
92 })?;
93
94 for (key, value) in params {
95 body = body.text(key.to_owned(), value.to_string());
96 }
97 }
98
99 let req = self
100 .client
101 .post("/audio/transcriptions")?
102 .body(body)
103 .map_err(|e| TranscriptionError::HttpError(e.into()))?;
104
105 let response = self.client.send_multipart::<Bytes>(req).await?;
106
107 let status = response.status();
108 let response_body = response.into_body().into_future().await?.to_vec();
109 if status.is_success() {
110 match serde_json::from_slice::<ApiResponse<TranscriptionResponse>>(&response_body)? {
111 ApiResponse::Ok(response) => response.try_into(),
112 ApiResponse::Err(api_error_response) => {
113 tracing::warn!(message = %api_error_response.message, "provider returned an error response");
114 Err(TranscriptionError::from_http_response(
115 status,
116 String::from_utf8_lossy(&response_body).into_owned(),
117 ))
118 }
119 }
120 } else {
121 let str = String::from_utf8_lossy(&response_body).to_string();
122 Err(TranscriptionError::from_http_response(status, str))
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::client::transcription::TranscriptionClient;
131 use crate::test_utils::RecordingHttpClient;
132 use crate::transcription::TranscriptionModel as _;
133
134 #[tokio::test]
135 async fn transcription_http_non_success_preserves_status_and_body() {
136 let body = r#"{"error":{"message":"bad audio","type":"invalid_request_error"}}"#;
137 let http_client =
138 RecordingHttpClient::with_error_response(http::StatusCode::BAD_REQUEST, body);
139 let client = Client::builder()
140 .api_key("test-key")
141 .http_client(http_client)
142 .build()
143 .expect("build client");
144 let model = client.transcription_model(WHISPER_1);
145
146 let error = match model
147 .transcription_request()
148 .data(vec![0u8; 16])
149 .send()
150 .await
151 {
152 Err(error) => error,
153 Ok(_) => panic!("transcription should fail with non-success status"),
154 };
155
156 assert!(matches!(error, TranscriptionError::HttpError(_)));
157 assert_eq!(
158 error.provider_response_status(),
159 Some(http::StatusCode::BAD_REQUEST)
160 );
161 assert_eq!(error.provider_response_body(), Some(body));
162 }
163}