swiftide_integrations/openai/
simple_prompt.rs1use async_openai::types::chat::ChatCompletionRequestUserMessageArgs;
6use async_trait::async_trait;
7use swiftide_core::{
8 SimplePrompt,
9 chat_completion::{Usage, errors::LanguageModelError},
10 prompt::Prompt,
11 util::debug_long_utf8,
12};
13
14use super::responses_api::{build_responses_request_from_prompt, response_to_chat_completion};
15use crate::openai::openai_error_to_language_model_error;
16
17use super::GenericOpenAI;
18use anyhow::Result;
19
20#[async_trait]
23impl<
24 C: async_openai::config::Config
25 + std::default::Default
26 + Sync
27 + Send
28 + std::fmt::Debug
29 + Clone
30 + 'static,
31> SimplePrompt for GenericOpenAI<C>
32{
33 #[cfg_attr(not(feature = "langfuse"), tracing::instrument(skip_all, err))]
47 #[cfg_attr(
48 feature = "langfuse",
49 tracing::instrument(skip_all, err, fields(langfuse.type = "GENERATION"))
50 )]
51 async fn prompt(&self, prompt: Prompt) -> Result<String, LanguageModelError> {
52 if self.is_responses_api_enabled() {
53 return self.prompt_via_responses_api(prompt).await;
54 }
55
56 let model = self
58 .default_options
59 .prompt_model
60 .as_ref()
61 .ok_or_else(|| LanguageModelError::PermanentError("Model not set".into()))?;
62
63 let request = self
65 .chat_completion_request_defaults()
66 .model(model)
67 .messages(vec![
68 ChatCompletionRequestUserMessageArgs::default()
69 .content(prompt.render()?)
70 .build()
71 .map_err(LanguageModelError::permanent)?
72 .into(),
73 ])
74 .build()
75 .map_err(LanguageModelError::permanent)?;
76
77 tracing::trace!(
79 model = &model,
80 messages = debug_long_utf8(
81 serde_json::to_string_pretty(&request.messages.last())
82 .map_err(LanguageModelError::permanent)?,
83 100
84 ),
85 "[SimplePrompt] Request to openai"
86 );
87
88 let tracking_request = request.clone();
91 let response = self
92 .client
93 .chat()
94 .create(request)
95 .await
96 .map_err(openai_error_to_language_model_error)?;
97
98 let message = response
99 .choices
100 .first()
101 .and_then(|choice| choice.message.content.clone())
102 .ok_or_else(|| {
103 LanguageModelError::PermanentError("Expected content in response".into())
104 })?;
105
106 let usage = response.usage.as_ref().map(Usage::from);
107
108 self.track_completion(
109 model,
110 usage.as_ref(),
111 Some(&tracking_request),
112 Some(&response),
113 );
114
115 Ok(message)
116 }
117}
118
119impl<
120 C: async_openai::config::Config
121 + std::default::Default
122 + Sync
123 + Send
124 + std::fmt::Debug
125 + Clone
126 + 'static,
127> GenericOpenAI<C>
128{
129 async fn prompt_via_responses_api(&self, prompt: Prompt) -> Result<String, LanguageModelError> {
130 let prompt_text = prompt.render().map_err(LanguageModelError::permanent)?;
131 let model = self
132 .default_options
133 .prompt_model
134 .as_ref()
135 .ok_or_else(|| LanguageModelError::PermanentError("Model not set".into()))?;
136
137 let create_request = build_responses_request_from_prompt(self, prompt_text)?;
138
139 let response = self
140 .client
141 .responses()
142 .create(create_request.clone())
143 .await
144 .map_err(openai_error_to_language_model_error)?;
145
146 let mut completion = response_to_chat_completion(response)?;
147
148 self.track_completion(
149 model,
150 completion.usage.as_ref(),
151 Some(&create_request),
152 Some(&completion),
153 );
154
155 completion.message.take().ok_or_else(|| {
156 LanguageModelError::PermanentError("Expected content in response".into())
157 })
158 }
159}
160
161#[allow(clippy::items_after_statements)]
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::openai::OpenAI;
166 use serde_json::Value;
167 use wiremock::{
168 Mock, MockServer, Request, Respond, ResponseTemplate,
169 matchers::{method, path},
170 };
171
172 #[test_log::test(tokio::test)]
173 async fn test_prompt_errors_when_model_missing() {
174 let openai = OpenAI::builder().build().unwrap();
175 let result = openai.prompt("hello".into()).await;
176 assert!(matches!(result, Err(LanguageModelError::PermanentError(_))));
177 }
178
179 #[test_log::test(tokio::test)]
180 async fn test_prompt_via_responses_api_returns_message() {
181 let mock_server = MockServer::start().await;
182
183 let response_body = serde_json::json!({
184 "created_at": 0,
185 "id": "resp",
186 "model": "gpt-4.1-mini",
187 "object": "response",
188 "status": "completed",
189 "output": [
190 {
191 "type": "message",
192 "id": "msg",
193 "role": "assistant",
194 "status": "completed",
195 "content": [
196 {"type": "output_text", "text": "Hello world", "annotations": []}
197 ]
198 }
199 ],
200 "usage": {
201 "input_tokens": 4,
202 "input_tokens_details": {"cached_tokens": 0},
203 "output_tokens": 2,
204 "output_tokens_details": {"reasoning_tokens": 0},
205 "total_tokens": 6
206 }
207 });
208
209 struct ValidatePromptRequest {
210 response: Value,
211 }
212
213 impl Respond for ValidatePromptRequest {
214 fn respond(&self, request: &Request) -> ResponseTemplate {
215 let payload: Value = serde_json::from_slice(&request.body).unwrap();
216 assert_eq!(payload["model"], self.response["model"]);
217 let items = payload["input"].as_array().expect("array input");
218 assert_eq!(items.len(), 1);
219 assert_eq!(items[0]["type"], "message");
220 ResponseTemplate::new(200).set_body_json(self.response.clone())
221 }
222 }
223
224 Mock::given(method("POST"))
225 .and(path("/responses"))
226 .respond_with(ValidatePromptRequest {
227 response: response_body,
228 })
229 .mount(&mock_server)
230 .await;
231
232 let config = async_openai::config::OpenAIConfig::new().with_api_base(mock_server.uri());
233 let client = async_openai::Client::with_config(config);
234
235 let openai = OpenAI::builder()
236 .client(client)
237 .default_prompt_model("gpt-4.1-mini")
238 .use_responses_api(true)
239 .build()
240 .unwrap();
241
242 let result = openai.prompt("Say hi".into()).await.unwrap();
243 assert_eq!(result, "Hello world");
244 }
245
246 #[test_log::test(tokio::test)]
247 async fn test_prompt_via_responses_api_missing_output_errors() {
248 let mock_server = MockServer::start().await;
249 let empty_response = serde_json::json!({
250 "created_at": 0,
251 "id": "resp",
252 "model": "gpt-4.1-mini",
253 "object": "response",
254 "output": [],
255 "status": "completed"
256 });
257
258 Mock::given(method("POST"))
259 .and(path("/responses"))
260 .respond_with(ResponseTemplate::new(200).set_body_json(empty_response))
261 .mount(&mock_server)
262 .await;
263
264 let config = async_openai::config::OpenAIConfig::new().with_api_base(mock_server.uri());
265 let client = async_openai::Client::with_config(config);
266
267 let openai = OpenAI::builder()
268 .client(client)
269 .default_prompt_model("gpt-4.1-mini")
270 .use_responses_api(true)
271 .build()
272 .unwrap();
273
274 let err = openai.prompt("test".into()).await.unwrap_err();
275 assert!(matches!(err, LanguageModelError::PermanentError(_)));
276 }
277}