1use super::responses_api::{
2 ConfigurableSystemInstructionsPlacement, ResponsesProviderExt, SystemInstructionsPlacement,
3};
4use crate::{
5 client::{self, BearerAuth, DebugExt, Provider},
6 http_client::HttpClientExt,
7 wasm_compat::{WasmCompatSend, WasmCompatSync},
8};
9use serde::Deserialize;
10use std::fmt::Debug;
11
12#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
13use crate::client::completion::CompletionClient;
14
15const OPENAI_API_BASE_URL: &str = "https://api.openai.com/v1";
19
20#[derive(Debug, Default, Clone, Copy)]
24pub struct OpenAIResponsesExt {
25 pub(crate) system_instructions_placement: SystemInstructionsPlacement,
26}
27
28#[derive(Debug, Default, Clone, Copy)]
29pub struct OpenAIResponsesExtBuilder;
30
31#[derive(Debug, Default, Clone, Copy)]
35pub struct OpenAICompletionsExt {
36 pub(crate) system_instructions_placement: SystemInstructionsPlacement,
40}
41
42#[derive(Debug, Default, Clone, Copy)]
43pub struct OpenAICompletionsExtBuilder;
44
45type OpenAIApiKey = BearerAuth;
46
47pub type Client<H = reqwest::Client> = client::Client<OpenAIResponsesExt, H>;
49pub type ClientBuilder<H = crate::markers::Missing> =
50 client::ClientBuilder<OpenAIResponsesExtBuilder, OpenAIApiKey, H>;
51
52pub type CompletionsClient<H = reqwest::Client> = client::Client<OpenAICompletionsExt, H>;
54pub type CompletionsClientBuilder<H = crate::markers::Missing> =
55 client::ClientBuilder<OpenAICompletionsExtBuilder, OpenAIApiKey, H>;
56
57impl Provider for OpenAIResponsesExt {
58 type Builder = OpenAIResponsesExtBuilder;
59 const VERIFY_PATH: &'static str = "/models";
60}
61
62impl ResponsesProviderExt for OpenAIResponsesExt {
63 fn system_instructions_placement(&self) -> SystemInstructionsPlacement {
64 self.system_instructions_placement
65 }
66}
67
68impl ConfigurableSystemInstructionsPlacement for OpenAIResponsesExt {}
69
70impl Provider for OpenAICompletionsExt {
71 type Builder = OpenAICompletionsExtBuilder;
72 const VERIFY_PATH: &'static str = "/models";
73}
74
75client::impl_capabilities!(
76 OpenAIResponsesExt,
77 completion = super::responses_api::ResponsesCompletionModel<H>,
78 embeddings = super::EmbeddingModel<H>,
79 transcription = super::TranscriptionModel<H>,
80 model_listing = super::OpenAIModelLister<H>,
81 image_generation = super::ImageGenerationModel<H>,
82 audio_generation = super::audio_generation::AudioGenerationModel<H>,
83);
84
85client::impl_capabilities!(
86 OpenAICompletionsExt,
87 completion = super::completion::CompletionModel<H>,
88 embeddings = super::GenericEmbeddingModel<OpenAICompletionsExt, H>,
89 transcription = super::CompletionsTranscriptionModel<H>,
90 model_listing = super::OpenAICompletionsModelLister<H>,
91 image_generation = super::CompletionsImageGenerationModel<H>,
92 audio_generation = super::audio_generation::CompletionsAudioGenerationModel<H>,
93);
94
95impl DebugExt for OpenAIResponsesExt {}
96
97impl DebugExt for OpenAICompletionsExt {}
98
99client::impl_default_provider_builder!(
100 OpenAIResponsesExtBuilder => OpenAIResponsesExt,
101 api_key = OpenAIApiKey,
102 base_url = OPENAI_API_BASE_URL,
103);
104client::impl_default_provider_builder!(
105 OpenAICompletionsExtBuilder => OpenAICompletionsExt,
106 api_key = OpenAIApiKey,
107 base_url = OPENAI_API_BASE_URL,
108);
109
110impl<H> Client<H>
111where
112 H: HttpClientExt
113 + Clone
114 + std::fmt::Debug
115 + Default
116 + WasmCompatSend
117 + WasmCompatSync
118 + 'static,
119{
120 pub fn with_system_instructions_placement(
125 self,
126 placement: SystemInstructionsPlacement,
127 ) -> Self {
128 let mut ext = *self.ext();
129 ext.system_instructions_placement = placement;
130 self.with_ext(ext)
131 }
132
133 pub fn with_system_instructions_as_messages(self) -> Self {
141 self.with_system_instructions_placement(SystemInstructionsPlacement::InputSystemMessages)
142 }
143
144 pub fn completions_api(self) -> CompletionsClient<H> {
147 let system_instructions_placement = self.ext().system_instructions_placement;
148 self.with_ext(OpenAICompletionsExt {
149 system_instructions_placement,
150 })
151 }
152}
153
154#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
155impl Client<reqwest::Client> {
156 pub fn responses_websocket_builder(
160 &self,
161 model: impl Into<String>,
162 ) -> super::responses_api::websocket::ResponsesWebSocketSessionBuilder {
163 super::responses_api::websocket::ResponsesWebSocketSessionBuilder::new(
164 self.completion_model(model),
165 )
166 }
167
168 pub async fn responses_websocket(
170 &self,
171 model: impl Into<String>,
172 ) -> Result<
173 super::responses_api::websocket::ResponsesWebSocketSession,
174 crate::completion::CompletionError,
175 > {
176 self.responses_websocket_builder(model).connect().await
177 }
178}
179
180impl<H> CompletionsClient<H>
181where
182 H: HttpClientExt
183 + Clone
184 + std::fmt::Debug
185 + Default
186 + WasmCompatSend
187 + WasmCompatSync
188 + 'static,
189{
190 pub fn responses_api(self) -> Client<H> {
195 let system_instructions_placement = self.ext().system_instructions_placement;
196 self.with_ext(OpenAIResponsesExt {
197 system_instructions_placement,
198 })
199 }
200}
201
202client::impl_provider_client!(
203 Client,
204 input = OpenAIApiKey,
205 api_key_env = "OPENAI_API_KEY",
206 base_url_env_first = "OPENAI_BASE_URL",
207);
208client::impl_provider_client!(
209 CompletionsClient,
210 input = OpenAIApiKey,
211 api_key_env = "OPENAI_API_KEY",
212 base_url_env_first = "OPENAI_BASE_URL",
213);
214
215#[derive(Debug)]
221pub struct ApiErrorResponse {
222 pub(crate) message: String,
223}
224
225impl<'de> Deserialize<'de> for ApiErrorResponse {
229 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230 where
231 D: serde::Deserializer<'de>,
232 {
233 Ok(Self {
234 message: crate::providers::internal::envelope::error_message(deserializer)?,
235 })
236 }
237}
238
239#[derive(Debug, Deserialize)]
240#[serde(untagged)]
241pub(crate) enum ApiResponse<T> {
242 Ok(T),
243 Err(ApiErrorResponse),
244}
245
246#[cfg(test)]
247mod tests {
248 use crate::client::{CompletionClient, EmbeddingsClient};
249 use crate::message;
250 use crate::message::ImageDetail;
251 use crate::providers::openai::{
252 AssistantContent, Function, ImageUrl, Message, ToolCall, ToolType, UserContent,
253 };
254 use serde_path_to_error::deserialize;
255
256 #[test]
257 fn test_deserialize_message() {
258 let assistant_message_json = r#"
259 {
260 "role": "assistant",
261 "content": "\n\nHello there, how may I assist you today?"
262 }
263 "#;
264
265 let assistant_message_json2 = r#"
266 {
267 "role": "assistant",
268 "content": [
269 {
270 "type": "text",
271 "text": "\n\nHello there, how may I assist you today?"
272 }
273 ],
274 "tool_calls": null
275 }
276 "#;
277
278 let assistant_message_json3 = r#"
279 {
280 "role": "assistant",
281 "tool_calls": [
282 {
283 "id": "call_h89ipqYUjEpCPI6SxspMnoUU",
284 "type": "function",
285 "function": {
286 "name": "subtract",
287 "arguments": "{\"x\": 2, \"y\": 5}"
288 }
289 }
290 ],
291 "content": null,
292 "refusal": null
293 }
294 "#;
295
296 let user_message_json = r#"
297 {
298 "role": "user",
299 "content": [
300 {
301 "type": "text",
302 "text": "What's in this image?"
303 },
304 {
305 "type": "image_url",
306 "image_url": {
307 "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
308 }
309 },
310 {
311 "type": "audio",
312 "input_audio": {
313 "data": "...",
314 "format": "mp3"
315 }
316 }
317 ]
318 }
319 "#;
320
321 let assistant_message: Message = {
322 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json);
323 deserialize(jd).unwrap_or_else(|err| {
324 panic!(
325 "Deserialization error at {} ({}:{}): {}",
326 err.path(),
327 err.inner().line(),
328 err.inner().column(),
329 err
330 );
331 })
332 };
333
334 let assistant_message2: Message = {
335 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json2);
336 deserialize(jd).unwrap_or_else(|err| {
337 panic!(
338 "Deserialization error at {} ({}:{}): {}",
339 err.path(),
340 err.inner().line(),
341 err.inner().column(),
342 err
343 );
344 })
345 };
346
347 let assistant_message3: Message = {
348 let jd: &mut serde_json::Deserializer<serde_json::de::StrRead<'_>> =
349 &mut serde_json::Deserializer::from_str(assistant_message_json3);
350 deserialize(jd).unwrap_or_else(|err| {
351 panic!(
352 "Deserialization error at {} ({}:{}): {}",
353 err.path(),
354 err.inner().line(),
355 err.inner().column(),
356 err
357 );
358 })
359 };
360
361 let user_message: Message = {
362 let jd = &mut serde_json::Deserializer::from_str(user_message_json);
363 deserialize(jd).unwrap_or_else(|err| {
364 panic!(
365 "Deserialization error at {} ({}:{}): {}",
366 err.path(),
367 err.inner().line(),
368 err.inner().column(),
369 err
370 );
371 })
372 };
373
374 match assistant_message {
375 Message::Assistant { content, .. } => {
376 assert_eq!(
377 content[0],
378 AssistantContent::Text {
379 text: "\n\nHello there, how may I assist you today?".to_string()
380 }
381 );
382 }
383 _ => panic!("Expected assistant message"),
384 }
385
386 match assistant_message2 {
387 Message::Assistant {
388 content,
389 tool_calls,
390 ..
391 } => {
392 assert_eq!(
393 content[0],
394 AssistantContent::Text {
395 text: "\n\nHello there, how may I assist you today?".to_string()
396 }
397 );
398
399 assert_eq!(tool_calls, vec![]);
400 }
401 _ => panic!("Expected assistant message"),
402 }
403
404 match assistant_message3 {
405 Message::Assistant {
406 content,
407 tool_calls,
408 refusal,
409 ..
410 } => {
411 assert!(content.is_empty());
412 assert!(refusal.is_none());
413 assert_eq!(
414 tool_calls[0],
415 ToolCall {
416 id: "call_h89ipqYUjEpCPI6SxspMnoUU".to_string(),
417 r#type: ToolType::Function,
418 function: Function {
419 name: "subtract".to_string(),
420 arguments: serde_json::json!({"x": 2, "y": 5}),
421 },
422 }
423 );
424 }
425 _ => panic!("Expected assistant message"),
426 }
427
428 match user_message {
429 Message::User { content, .. } => {
430 let (first, second) = {
431 let mut iter = content.into_iter();
432 (iter.next().unwrap(), iter.next().unwrap())
433 };
434 assert_eq!(
435 first,
436 UserContent::Text {
437 text: "What's in this image?".to_string()
438 }
439 );
440 assert_eq!(second, UserContent::Image { image_url: ImageUrl { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg".to_string(), detail: None } });
441 }
442 _ => panic!("Expected user message"),
443 }
444 }
445
446 #[test]
447 fn test_message_to_message_conversion() {
448 let user_message = message::Message::User {
449 content: vec![message::UserContent::text("Hello")],
450 };
451
452 let assistant_message = message::Message::Assistant {
453 id: None,
454 content: vec![message::AssistantContent::text("Hi there!")],
455 };
456
457 let converted_user_message: Vec<Message> = user_message.clone().try_into().unwrap();
458 let converted_assistant_message: Vec<Message> =
459 assistant_message.clone().try_into().unwrap();
460
461 match converted_user_message[0].clone() {
462 Message::User { content, .. } => {
463 assert_eq!(
464 content.first(),
465 Some(&UserContent::Text {
466 text: "Hello".to_string()
467 })
468 );
469 }
470 _ => panic!("Expected user message"),
471 }
472
473 match converted_assistant_message[0].clone() {
474 Message::Assistant { content, .. } => {
475 assert_eq!(
476 content[0].clone(),
477 AssistantContent::Text {
478 text: "Hi there!".to_string()
479 }
480 );
481 }
482 _ => panic!("Expected assistant message"),
483 }
484
485 let original_user_message: message::Message =
486 converted_user_message[0].clone().try_into().unwrap();
487 let original_assistant_message: message::Message =
488 converted_assistant_message[0].clone().try_into().unwrap();
489
490 assert_eq!(original_user_message, user_message);
491 assert_eq!(original_assistant_message, assistant_message);
492 }
493
494 #[test]
495 fn test_message_from_message_conversion() {
496 let user_message = Message::User {
497 content: vec![UserContent::Text {
498 text: "Hello".to_string(),
499 }],
500 name: None,
501 };
502
503 let assistant_message = Message::Assistant {
504 content: vec![AssistantContent::Text {
505 text: "Hi there!".to_string(),
506 }],
507 reasoning: None,
508 refusal: None,
509 audio: None,
510 name: None,
511 tool_calls: vec![],
512 reasoning_details: vec![],
513 images: vec![],
514 };
515
516 let converted_user_message: message::Message = user_message.clone().try_into().unwrap();
517 let converted_assistant_message: message::Message =
518 assistant_message.clone().try_into().unwrap();
519
520 match converted_user_message.clone() {
521 message::Message::User { content } => {
522 assert_eq!(content.first(), Some(&message::UserContent::text("Hello")));
523 }
524 _ => panic!("Expected user message"),
525 }
526
527 match converted_assistant_message.clone() {
528 message::Message::Assistant { content, .. } => {
529 assert_eq!(
530 content.first(),
531 Some(&message::AssistantContent::text("Hi there!"))
532 );
533 }
534 _ => panic!("Expected assistant message"),
535 }
536
537 let original_user_message: Vec<Message> = converted_user_message.try_into().unwrap();
538 let original_assistant_message: Vec<Message> =
539 converted_assistant_message.try_into().unwrap();
540
541 assert_eq!(original_user_message[0], user_message);
542 assert_eq!(original_assistant_message[0], assistant_message);
543 }
544
545 #[test]
546 fn test_user_message_single_text_serializes_as_string() {
547 let user_message = Message::User {
548 content: vec![UserContent::Text {
549 text: "Hello world".to_string(),
550 }],
551 name: None,
552 };
553
554 let serialized = serde_json::to_value(&user_message).unwrap();
555
556 assert_eq!(serialized["role"], "user");
557 assert_eq!(serialized["content"], "Hello world");
558 }
559
560 #[test]
561 fn test_user_message_multiple_parts_serializes_as_array() {
562 let user_message = Message::User {
563 content: vec![
564 UserContent::Text {
565 text: "What's in this image?".to_string(),
566 },
567 UserContent::Image {
568 image_url: ImageUrl {
569 url: "https://example.com/image.jpg".to_string(),
570 detail: Some(ImageDetail::default()),
571 },
572 },
573 ],
574 name: None,
575 };
576
577 let serialized = serde_json::to_value(&user_message).unwrap();
578
579 assert_eq!(serialized["role"], "user");
580 assert!(serialized["content"].is_array());
581 assert_eq!(serialized["content"].as_array().unwrap().len(), 2);
582 }
583
584 #[test]
585 fn test_user_message_single_image_serializes_as_array() {
586 let user_message = Message::User {
587 content: vec![UserContent::Image {
588 image_url: ImageUrl {
589 url: "https://example.com/image.jpg".to_string(),
590 detail: Some(ImageDetail::default()),
591 },
592 }],
593 name: None,
594 };
595
596 let serialized = serde_json::to_value(&user_message).unwrap();
597
598 assert_eq!(serialized["role"], "user");
599 assert!(serialized["content"].is_array());
601 }
602 #[test]
603 fn test_client_initialization() {
604 let _client =
605 crate::providers::openai::Client::new("dummy-key").expect("Client::new() failed");
606 let _client_from_builder = crate::providers::openai::Client::builder()
607 .api_key("dummy-key")
608 .build()
609 .expect("Client::builder() failed");
610 }
611
612 #[test]
613 fn test_legacy_chat_completion_model_type_annotation_still_compiles() {
614 let client = crate::providers::openai::Client::new("dummy-key")
615 .expect("Client::new() failed")
616 .completions_api();
617
618 let _model: crate::providers::openai::completion::CompletionModel<reqwest::Client> =
619 client.completion_model("gpt-4o");
620 }
621
622 #[test]
623 fn test_legacy_embedding_model_type_annotation_still_compiles() {
624 let client =
625 crate::providers::openai::Client::new("dummy-key").expect("Client::new() failed");
626
627 let _model: crate::providers::openai::EmbeddingModel<reqwest::Client> =
628 client.embedding_model(crate::providers::openai::TEXT_EMBEDDING_3_SMALL);
629 }
630
631 #[test]
632 fn api_switch_preserves_non_completion_capabilities() {
633 use crate::client::ModelListingClient;
634 use crate::client::transcription::TranscriptionClient;
635
636 let client = crate::providers::openai::Client::new("dummy-key")
637 .expect("Client::new() failed")
638 .completions_api();
639
640 let _: crate::providers::openai::GenericEmbeddingModel<
641 crate::providers::openai::OpenAICompletionsExt,
642 reqwest::Client,
643 > = client.embedding_model(crate::providers::openai::TEXT_EMBEDDING_3_SMALL);
644 let _: crate::providers::openai::CompletionsTranscriptionModel =
645 client.transcription_model(crate::providers::openai::WHISPER_1);
646
647 fn assert_model_listing<T: ModelListingClient>(_: &T) {}
648 assert_model_listing(&client);
649
650 #[cfg(feature = "image")]
651 {
652 use crate::client::image_generation::ImageGenerationClient;
653 let _: crate::providers::openai::CompletionsImageGenerationModel =
654 client.image_generation_model(crate::providers::openai::DALL_E_3);
655 }
656
657 #[cfg(feature = "audio")]
658 {
659 use crate::client::audio_generation::AudioGenerationClient;
660 let _: crate::providers::openai::audio_generation::CompletionsAudioGenerationModel =
661 client.audio_generation_model(crate::providers::openai::TTS_1);
662 }
663 }
664}