1use super::responses_api::{ResponsesProviderExt, SystemInstructionsPlacement};
2use crate::{
3 client::{
4 self, BearerAuth, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder,
5 ProviderClient,
6 },
7 extractor::ExtractorBuilder,
8 http_client::{self, HttpClientExt},
9 prelude::CompletionClient,
10 wasm_compat::{WasmCompatSend, WasmCompatSync},
11};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::fmt::Debug;
15
16const OPENAI_API_BASE_URL: &str = "https://api.openai.com/v1";
20
21#[derive(Debug, Default, Clone, Copy)]
25pub struct OpenAIResponsesExt {
26 pub(crate) system_instructions_placement: SystemInstructionsPlacement,
27}
28
29#[derive(Debug, Default, Clone, Copy)]
30pub struct OpenAIResponsesExtBuilder;
31
32#[derive(Debug, Default, Clone, Copy)]
36pub struct OpenAICompletionsExt {
37 pub(crate) system_instructions_placement: SystemInstructionsPlacement,
41}
42
43#[derive(Debug, Default, Clone, Copy)]
44pub struct OpenAICompletionsExtBuilder;
45
46type OpenAIApiKey = BearerAuth;
47
48pub type Client<H = reqwest::Client> = client::Client<OpenAIResponsesExt, H>;
50pub type ClientBuilder<H = crate::markers::Missing> =
51 client::ClientBuilder<OpenAIResponsesExtBuilder, OpenAIApiKey, H>;
52
53pub type CompletionsClient<H = reqwest::Client> = client::Client<OpenAICompletionsExt, H>;
55pub type CompletionsClientBuilder<H = crate::markers::Missing> =
56 client::ClientBuilder<OpenAICompletionsExtBuilder, OpenAIApiKey, H>;
57
58impl Provider for OpenAIResponsesExt {
59 type Builder = OpenAIResponsesExtBuilder;
60 const VERIFY_PATH: &'static str = "/models";
61}
62
63impl ResponsesProviderExt for OpenAIResponsesExt {
64 fn system_instructions_placement(&self) -> SystemInstructionsPlacement {
65 self.system_instructions_placement
66 }
67}
68
69impl Provider for OpenAICompletionsExt {
70 type Builder = OpenAICompletionsExtBuilder;
71 const VERIFY_PATH: &'static str = "/models";
72}
73
74impl<H> Capabilities<H> for OpenAIResponsesExt {
75 type Completion = Capable<super::responses_api::ResponsesCompletionModel<H>>;
76 type Embeddings = Capable<super::EmbeddingModel<H>>;
77 type Transcription = Capable<super::TranscriptionModel<H>>;
78 type ModelListing = Capable<super::OpenAIModelLister<H>>;
79 #[cfg(feature = "image")]
80 type ImageGeneration = Capable<super::ImageGenerationModel<H>>;
81 #[cfg(feature = "audio")]
82 type AudioGeneration = Capable<super::audio_generation::AudioGenerationModel<H>>;
83 type Rerank = Nothing;
84}
85
86impl<H> Capabilities<H> for OpenAICompletionsExt {
87 type Completion = Capable<super::completion::CompletionModel<H>>;
88 type Embeddings = Capable<super::GenericEmbeddingModel<OpenAICompletionsExt, H>>;
89 type Transcription = Capable<super::TranscriptionModel<H>>;
90 type ModelListing = Capable<super::OpenAIModelLister<H>>;
91 #[cfg(feature = "image")]
92 type ImageGeneration = Capable<super::ImageGenerationModel<H>>;
93 #[cfg(feature = "audio")]
94 type AudioGeneration = Capable<super::audio_generation::AudioGenerationModel<H>>;
95 type Rerank = Nothing;
96}
97
98impl DebugExt for OpenAIResponsesExt {}
99
100impl DebugExt for OpenAICompletionsExt {}
101
102impl ProviderBuilder for OpenAIResponsesExtBuilder {
103 type Extension<H>
104 = OpenAIResponsesExt
105 where
106 H: HttpClientExt;
107 type ApiKey = OpenAIApiKey;
108
109 const BASE_URL: &'static str = OPENAI_API_BASE_URL;
110
111 fn build<H>(
112 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
113 ) -> http_client::Result<Self::Extension<H>>
114 where
115 H: HttpClientExt,
116 {
117 Ok(OpenAIResponsesExt::default())
118 }
119}
120
121impl ProviderBuilder for OpenAICompletionsExtBuilder {
122 type Extension<H>
123 = OpenAICompletionsExt
124 where
125 H: HttpClientExt;
126 type ApiKey = OpenAIApiKey;
127
128 const BASE_URL: &'static str = OPENAI_API_BASE_URL;
129
130 fn build<H>(
131 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
132 ) -> http_client::Result<Self::Extension<H>>
133 where
134 H: HttpClientExt,
135 {
136 Ok(OpenAICompletionsExt::default())
137 }
138}
139
140impl<H> Client<H>
141where
142 H: HttpClientExt
143 + Clone
144 + std::fmt::Debug
145 + Default
146 + WasmCompatSend
147 + WasmCompatSync
148 + 'static,
149{
150 pub fn extractor<U>(
153 &self,
154 model: impl Into<String>,
155 ) -> ExtractorBuilder<super::responses_api::ResponsesCompletionModel<H>, U>
156 where
157 U: JsonSchema + for<'a> Deserialize<'a> + Serialize + WasmCompatSend + WasmCompatSync,
158 {
159 ExtractorBuilder::new(self.completion_model(model))
160 }
161
162 pub fn with_system_instructions_placement(
169 self,
170 placement: SystemInstructionsPlacement,
171 ) -> Self {
172 let mut ext = *self.ext();
173 ext.system_instructions_placement = placement;
174 self.with_ext(ext)
175 }
176
177 pub fn with_system_instructions_as_messages(self) -> Self {
186 self.with_system_instructions_placement(SystemInstructionsPlacement::InputSystemMessages)
187 }
188
189 pub fn completions_api(self) -> CompletionsClient<H> {
192 let system_instructions_placement = self.ext().system_instructions_placement;
193 self.with_ext(OpenAICompletionsExt {
194 system_instructions_placement,
195 })
196 }
197}
198
199#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
200impl Client<reqwest::Client> {
201 pub fn responses_websocket_builder(
205 &self,
206 model: impl Into<String>,
207 ) -> super::responses_api::websocket::ResponsesWebSocketSessionBuilder {
208 super::responses_api::websocket::ResponsesWebSocketSessionBuilder::new(
209 self.completion_model(model),
210 )
211 }
212
213 pub async fn responses_websocket(
215 &self,
216 model: impl Into<String>,
217 ) -> Result<
218 super::responses_api::websocket::ResponsesWebSocketSession,
219 crate::completion::CompletionError,
220 > {
221 self.responses_websocket_builder(model).connect().await
222 }
223}
224
225impl<H> CompletionsClient<H>
226where
227 H: HttpClientExt
228 + Clone
229 + std::fmt::Debug
230 + Default
231 + WasmCompatSend
232 + WasmCompatSync
233 + 'static,
234{
235 pub fn extractor<U>(
238 &self,
239 model: impl Into<String>,
240 ) -> ExtractorBuilder<super::completion::CompletionModel<H>, U>
241 where
242 U: JsonSchema + for<'a> Deserialize<'a> + Serialize + WasmCompatSend + WasmCompatSync,
243 {
244 ExtractorBuilder::new(self.completion_model(model))
245 }
246
247 pub fn responses_api(self) -> Client<H> {
252 let system_instructions_placement = self.ext().system_instructions_placement;
253 self.with_ext(OpenAIResponsesExt {
254 system_instructions_placement,
255 })
256 }
257}
258
259impl ProviderClient for Client {
260 type Input = OpenAIApiKey;
261 type Error = crate::client::ProviderClientError;
262
263 fn from_env() -> Result<Self, Self::Error> {
265 let base_url = crate::client::optional_env_var("OPENAI_BASE_URL")?;
266 let api_key = crate::client::required_env_var("OPENAI_API_KEY")?;
267
268 let mut builder = Client::builder().api_key(&api_key);
269
270 if let Some(base) = base_url {
271 builder = builder.base_url(&base);
272 }
273
274 builder.build().map_err(Into::into)
275 }
276
277 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
278 Self::new(input).map_err(Into::into)
279 }
280}
281
282impl ProviderClient for CompletionsClient {
283 type Input = OpenAIApiKey;
284 type Error = crate::client::ProviderClientError;
285
286 fn from_env() -> Result<Self, Self::Error> {
288 let base_url = crate::client::optional_env_var("OPENAI_BASE_URL")?;
289 let api_key = crate::client::required_env_var("OPENAI_API_KEY")?;
290
291 let mut builder = CompletionsClient::builder().api_key(&api_key);
292
293 if let Some(base) = base_url {
294 builder = builder.base_url(&base);
295 }
296
297 builder.build().map_err(Into::into)
298 }
299
300 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
301 Self::new(input).map_err(Into::into)
302 }
303}
304
305#[derive(Debug, Deserialize)]
311pub struct ApiErrorResponse {
312 #[serde(default, alias = "error", deserialize_with = "error_message_or_value")]
313 pub(crate) message: String,
314}
315
316fn error_message_or_value<'de, D>(deserializer: D) -> Result<String, D::Error>
317where
318 D: serde::Deserializer<'de>,
319{
320 let value = serde_json::Value::deserialize(deserializer)?;
321 Ok(match value {
322 serde_json::Value::String(message) => message,
323 other => other.to_string(),
324 })
325}
326
327#[derive(Debug, Deserialize)]
328#[serde(untagged)]
329pub(crate) enum ApiResponse<T> {
330 Ok(T),
331 Err(ApiErrorResponse),
332}
333
334#[cfg(test)]
335mod tests {
336 use crate::client::{CompletionClient, EmbeddingsClient};
337 use crate::message::ImageDetail;
338 use crate::providers::openai::{
339 AssistantContent, Function, ImageUrl, Message, ToolCall, ToolType, UserContent,
340 };
341 use crate::{OneOrMany, message};
342 use serde_path_to_error::deserialize;
343
344 #[test]
345 fn test_deserialize_message() {
346 let assistant_message_json = r#"
347 {
348 "role": "assistant",
349 "content": "\n\nHello there, how may I assist you today?"
350 }
351 "#;
352
353 let assistant_message_json2 = r#"
354 {
355 "role": "assistant",
356 "content": [
357 {
358 "type": "text",
359 "text": "\n\nHello there, how may I assist you today?"
360 }
361 ],
362 "tool_calls": null
363 }
364 "#;
365
366 let assistant_message_json3 = r#"
367 {
368 "role": "assistant",
369 "tool_calls": [
370 {
371 "id": "call_h89ipqYUjEpCPI6SxspMnoUU",
372 "type": "function",
373 "function": {
374 "name": "subtract",
375 "arguments": "{\"x\": 2, \"y\": 5}"
376 }
377 }
378 ],
379 "content": null,
380 "refusal": null
381 }
382 "#;
383
384 let user_message_json = r#"
385 {
386 "role": "user",
387 "content": [
388 {
389 "type": "text",
390 "text": "What's in this image?"
391 },
392 {
393 "type": "image_url",
394 "image_url": {
395 "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"
396 }
397 },
398 {
399 "type": "audio",
400 "input_audio": {
401 "data": "...",
402 "format": "mp3"
403 }
404 }
405 ]
406 }
407 "#;
408
409 let assistant_message: Message = {
410 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json);
411 deserialize(jd).unwrap_or_else(|err| {
412 panic!(
413 "Deserialization error at {} ({}:{}): {}",
414 err.path(),
415 err.inner().line(),
416 err.inner().column(),
417 err
418 );
419 })
420 };
421
422 let assistant_message2: Message = {
423 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json2);
424 deserialize(jd).unwrap_or_else(|err| {
425 panic!(
426 "Deserialization error at {} ({}:{}): {}",
427 err.path(),
428 err.inner().line(),
429 err.inner().column(),
430 err
431 );
432 })
433 };
434
435 let assistant_message3: Message = {
436 let jd: &mut serde_json::Deserializer<serde_json::de::StrRead<'_>> =
437 &mut serde_json::Deserializer::from_str(assistant_message_json3);
438 deserialize(jd).unwrap_or_else(|err| {
439 panic!(
440 "Deserialization error at {} ({}:{}): {}",
441 err.path(),
442 err.inner().line(),
443 err.inner().column(),
444 err
445 );
446 })
447 };
448
449 let user_message: Message = {
450 let jd = &mut serde_json::Deserializer::from_str(user_message_json);
451 deserialize(jd).unwrap_or_else(|err| {
452 panic!(
453 "Deserialization error at {} ({}:{}): {}",
454 err.path(),
455 err.inner().line(),
456 err.inner().column(),
457 err
458 );
459 })
460 };
461
462 match assistant_message {
463 Message::Assistant { content, .. } => {
464 assert_eq!(
465 content[0],
466 AssistantContent::Text {
467 text: "\n\nHello there, how may I assist you today?".to_string()
468 }
469 );
470 }
471 _ => panic!("Expected assistant message"),
472 }
473
474 match assistant_message2 {
475 Message::Assistant {
476 content,
477 tool_calls,
478 ..
479 } => {
480 assert_eq!(
481 content[0],
482 AssistantContent::Text {
483 text: "\n\nHello there, how may I assist you today?".to_string()
484 }
485 );
486
487 assert_eq!(tool_calls, vec![]);
488 }
489 _ => panic!("Expected assistant message"),
490 }
491
492 match assistant_message3 {
493 Message::Assistant {
494 content,
495 tool_calls,
496 refusal,
497 ..
498 } => {
499 assert!(content.is_empty());
500 assert!(refusal.is_none());
501 assert_eq!(
502 tool_calls[0],
503 ToolCall {
504 id: "call_h89ipqYUjEpCPI6SxspMnoUU".to_string(),
505 r#type: ToolType::Function,
506 function: Function {
507 name: "subtract".to_string(),
508 arguments: serde_json::json!({"x": 2, "y": 5}),
509 },
510 }
511 );
512 }
513 _ => panic!("Expected assistant message"),
514 }
515
516 match user_message {
517 Message::User { content, .. } => {
518 let (first, second) = {
519 let mut iter = content.into_iter();
520 (iter.next().unwrap(), iter.next().unwrap())
521 };
522 assert_eq!(
523 first,
524 UserContent::Text {
525 text: "What's in this image?".to_string()
526 }
527 );
528 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 } });
529 }
530 _ => panic!("Expected user message"),
531 }
532 }
533
534 #[test]
535 fn test_message_to_message_conversion() {
536 let user_message = message::Message::User {
537 content: OneOrMany::one(message::UserContent::text("Hello")),
538 };
539
540 let assistant_message = message::Message::Assistant {
541 id: None,
542 content: OneOrMany::one(message::AssistantContent::text("Hi there!")),
543 };
544
545 let converted_user_message: Vec<Message> = user_message.clone().try_into().unwrap();
546 let converted_assistant_message: Vec<Message> =
547 assistant_message.clone().try_into().unwrap();
548
549 match converted_user_message[0].clone() {
550 Message::User { content, .. } => {
551 assert_eq!(
552 content.first(),
553 UserContent::Text {
554 text: "Hello".to_string()
555 }
556 );
557 }
558 _ => panic!("Expected user message"),
559 }
560
561 match converted_assistant_message[0].clone() {
562 Message::Assistant { content, .. } => {
563 assert_eq!(
564 content[0].clone(),
565 AssistantContent::Text {
566 text: "Hi there!".to_string()
567 }
568 );
569 }
570 _ => panic!("Expected assistant message"),
571 }
572
573 let original_user_message: message::Message =
574 converted_user_message[0].clone().try_into().unwrap();
575 let original_assistant_message: message::Message =
576 converted_assistant_message[0].clone().try_into().unwrap();
577
578 assert_eq!(original_user_message, user_message);
579 assert_eq!(original_assistant_message, assistant_message);
580 }
581
582 #[test]
583 fn test_message_from_message_conversion() {
584 let user_message = Message::User {
585 content: OneOrMany::one(UserContent::Text {
586 text: "Hello".to_string(),
587 }),
588 name: None,
589 };
590
591 let assistant_message = Message::Assistant {
592 content: vec![AssistantContent::Text {
593 text: "Hi there!".to_string(),
594 }],
595 reasoning: None,
596 refusal: None,
597 audio: None,
598 name: None,
599 tool_calls: vec![],
600 reasoning_details: vec![],
601 images: vec![],
602 };
603
604 let converted_user_message: message::Message = user_message.clone().try_into().unwrap();
605 let converted_assistant_message: message::Message =
606 assistant_message.clone().try_into().unwrap();
607
608 match converted_user_message.clone() {
609 message::Message::User { content } => {
610 assert_eq!(content.first(), message::UserContent::text("Hello"));
611 }
612 _ => panic!("Expected user message"),
613 }
614
615 match converted_assistant_message.clone() {
616 message::Message::Assistant { content, .. } => {
617 assert_eq!(
618 content.first(),
619 message::AssistantContent::text("Hi there!")
620 );
621 }
622 _ => panic!("Expected assistant message"),
623 }
624
625 let original_user_message: Vec<Message> = converted_user_message.try_into().unwrap();
626 let original_assistant_message: Vec<Message> =
627 converted_assistant_message.try_into().unwrap();
628
629 assert_eq!(original_user_message[0], user_message);
630 assert_eq!(original_assistant_message[0], assistant_message);
631 }
632
633 #[test]
634 fn test_user_message_single_text_serializes_as_string() {
635 let user_message = Message::User {
636 content: OneOrMany::one(UserContent::Text {
637 text: "Hello world".to_string(),
638 }),
639 name: None,
640 };
641
642 let serialized = serde_json::to_value(&user_message).unwrap();
643
644 assert_eq!(serialized["role"], "user");
645 assert_eq!(serialized["content"], "Hello world");
646 }
647
648 #[test]
649 fn test_user_message_multiple_parts_serializes_as_array() {
650 let user_message = Message::User {
651 content: OneOrMany::many(vec![
652 UserContent::Text {
653 text: "What's in this image?".to_string(),
654 },
655 UserContent::Image {
656 image_url: ImageUrl {
657 url: "https://example.com/image.jpg".to_string(),
658 detail: Some(ImageDetail::default()),
659 },
660 },
661 ])
662 .unwrap(),
663 name: None,
664 };
665
666 let serialized = serde_json::to_value(&user_message).unwrap();
667
668 assert_eq!(serialized["role"], "user");
669 assert!(serialized["content"].is_array());
670 assert_eq!(serialized["content"].as_array().unwrap().len(), 2);
671 }
672
673 #[test]
674 fn test_user_message_single_image_serializes_as_array() {
675 let user_message = Message::User {
676 content: OneOrMany::one(UserContent::Image {
677 image_url: ImageUrl {
678 url: "https://example.com/image.jpg".to_string(),
679 detail: Some(ImageDetail::default()),
680 },
681 }),
682 name: None,
683 };
684
685 let serialized = serde_json::to_value(&user_message).unwrap();
686
687 assert_eq!(serialized["role"], "user");
688 assert!(serialized["content"].is_array());
690 }
691 #[test]
692 fn test_client_initialization() {
693 let _client =
694 crate::providers::openai::Client::new("dummy-key").expect("Client::new() failed");
695 let _client_from_builder = crate::providers::openai::Client::builder()
696 .api_key("dummy-key")
697 .build()
698 .expect("Client::builder() failed");
699 }
700
701 #[test]
702 fn test_legacy_chat_completion_model_type_annotation_still_compiles() {
703 let client = crate::providers::openai::Client::new("dummy-key")
704 .expect("Client::new() failed")
705 .completions_api();
706
707 let _model: crate::providers::openai::completion::CompletionModel<reqwest::Client> =
708 client.completion_model("gpt-4o");
709 }
710
711 #[test]
712 fn test_legacy_embedding_model_type_annotation_still_compiles() {
713 let client =
714 crate::providers::openai::Client::new("dummy-key").expect("Client::new() failed");
715
716 let _model: crate::providers::openai::EmbeddingModel<reqwest::Client> =
717 client.embedding_model(crate::providers::openai::TEXT_EMBEDDING_3_SMALL);
718 }
719}