1use crate::wire::Flow;
10use serde::{Deserialize, Serialize};
11
12use crate::embeddings;
13use crate::error::EncodeError;
14use crate::error::ProviderError;
15use crate::model::{ModelInfo, ModelList};
16use crate::operation::{
17 Embedding, ModelListing, ModelPage, Rerank as RerankOp, Transcription, Verify as VerifyOp,
18};
19use crate::providers::internal::wire::classify_untyped_line;
20use crate::providers::openai::embedding::Usage;
21use crate::providers::openai::embedding::{
22 CompatibleEmbeddingResponse, EncodingFormat, model_dimensions_from_identifier,
23};
24use crate::transcription::TranscriptionRequest;
25use crate::wire::{
26 Body, Capabilities, Decoder, Descriptor, DocumentDecoder, Encoded, Framing, Mode, Out, Wire,
27 WireEvent, WireFrame,
28};
29
30use super::OpenAIConfig;
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub enum ImageBody {
35 #[default]
37 OpenAi,
38 Xai,
41 Hyperbolic,
43 Venice,
45 HuggingFace,
48 MiniMax,
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum SpeechBody {
58 #[default]
60 OpenAi,
61 Xai,
63 Hyperbolic,
69 MiniMax,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum TranscriptionBody {
78 Multipart,
81 InputAudioJson,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum DimensionsField {
92 Dimensions,
94 OutputDimension,
96 Ignored,
99}
100
101impl DimensionsField {
102 pub const fn name(self) -> Option<&'static str> {
108 match self {
109 Self::Dimensions => Some("dimensions"),
110 Self::OutputDimension => Some("output_dimension"),
111 Self::Ignored => None,
112 }
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum AcceptedWidths {
119 Fixed,
123 Range {
126 min: usize,
128 max: usize,
130 requirement: &'static str,
133 },
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142#[non_exhaustive]
143pub struct ModelWidth {
144 pub model: &'static str,
146 pub default: Option<usize>,
149 pub accepted: AcceptedWidths,
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155#[non_exhaustive]
156pub struct RerankQuirks {
157 pub path: &'static str,
159 pub max_documents: usize,
161 pub sends_model_field: bool,
163}
164
165impl RerankQuirks {
166 pub const fn unsupported() -> Self {
168 Self {
169 path: "",
170 max_documents: 0,
171 sends_model_field: true,
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178#[non_exhaustive]
179pub struct EmbeddingQuirks {
180 pub max_documents: usize,
182 pub requires_usage: bool,
184 pub supports_encoding_format: bool,
186 pub supports_user: bool,
188 pub sends_model_field: bool,
191 pub dimensions: DimensionsField,
193 pub widths: &'static [ModelWidth],
196 pub refuse_zero_width: Option<&'static str>,
200}
201
202impl EmbeddingQuirks {
203 pub const fn openai() -> Self {
205 Self {
206 max_documents: 1024,
207 requires_usage: true,
208 supports_encoding_format: true,
209 supports_user: true,
210 sends_model_field: true,
211 dimensions: DimensionsField::Dimensions,
212 widths: &[],
214 refuse_zero_width: None,
216 }
217 }
218}
219
220impl super::SubRoute {
221 pub fn serves_model_routed_endpoints(&self) -> bool {
224 matches!(self, Self::HFInference)
225 }
226}
227
228impl OpenAIConfig {
229 pub fn with_audio_api_version(mut self, api_version: impl Into<String>) -> Self {
231 self.audio_api_version = Some(api_version.into());
232 self
233 }
234
235 pub(crate) fn embedding(&self, model: impl Into<String>, ndims: Option<usize>) -> Embeddings {
237 Embeddings::new(self.clone(), model, ndims)
238 }
239
240 pub(crate) fn rerank(&self, model: impl Into<String>) -> Rerank {
242 Rerank::new(self.clone(), model)
243 }
244
245 pub(crate) fn transcription(&self, model: impl Into<String>) -> Transcriptions {
247 Transcriptions::new(self.clone(), model)
248 }
249
250 pub(crate) fn models(&self) -> Models {
252 Models::new(self.clone())
253 }
254
255 pub(crate) fn verify(&self) -> Verify {
257 Verify::new(self.clone())
258 }
259
260 #[cfg(feature = "image")]
262 pub(crate) fn image_generation(&self, model: impl Into<String>) -> Images {
263 Images::new(self.clone(), model)
264 }
265
266 #[cfg(feature = "audio")]
268 pub(crate) fn audio_generation(&self, model: impl Into<String>) -> Speech {
269 Speech::new(self.clone(), model)
270 }
271
272 #[cfg(feature = "audio")]
274 pub(crate) fn speech_api_version(&self) -> Option<&str> {
275 self.audio_api_version
276 .as_deref()
277 .or(self.api_version.as_deref())
278 }
279
280 pub(crate) fn modality_uri(
283 &self,
284 endpoint: &str,
285 fixed: &'static str,
286 model: &str,
287 ) -> Result<String, String> {
288 if !self.dialect.quirks.model_is_modality_path {
289 return Ok(self.uri(fixed, self.deployment(model)));
290 }
291 let route = self.route();
292 if !route.serves_model_routed_endpoints() {
293 return Err(format!(
294 "{endpoint} endpoint is not supported yet for {route}"
295 ));
296 }
297 Ok(format!(
298 "{}/{}",
299 self.base_url.trim_end_matches('/'),
300 model.trim_start_matches('/')
301 ))
302 }
303}
304
305pub(super) const MISTRAL_EMBEDDING_WIDTHS: &[ModelWidth] = &[
316 ModelWidth {
317 model: crate::providers::mistral::embedding::MISTRAL_EMBED,
318 default: Some(1_024),
319 accepted: AcceptedWidths::Fixed,
320 },
321 ModelWidth {
322 model: "mistral-embed-2312",
323 default: Some(1_024),
324 accepted: AcceptedWidths::Fixed,
325 },
326 ModelWidth {
327 model: crate::providers::mistral::embedding::CODESTRAL_EMBED,
328 default: None,
331 accepted: AcceptedWidths::Range {
332 min: 0,
335 max: 3_072,
336 requirement: "to be at most 3072 for Codestral Embed",
337 },
338 },
339 ModelWidth {
340 model: "codestral-embed-2505",
341 default: None,
342 accepted: AcceptedWidths::Range {
343 min: 0,
344 max: 3_072,
345 requirement: "to be at most 3072 for Codestral Embed",
346 },
347 },
348];
349
350pub(super) const DOUBLEWORD_EMBEDDING_WIDTHS: &[ModelWidth] = &[ModelWidth {
354 model: crate::providers::doubleword::QWEN3_EMBEDDING_8B,
355 default: Some(4_096),
356 accepted: AcceptedWidths::Range {
357 min: 32,
358 max: 4_096,
359 requirement: "to be between 32 and 4096",
360 },
361}];
362
363pub(crate) fn json_post(
365 provider: &OpenAIConfig,
366 path: &str,
367 deployment: Option<&str>,
368 body: &serde_json::Value,
369) -> Result<Encoded, EncodeError> {
370 json_post_to(provider, provider.uri(path, deployment), body)
371}
372
373fn json_post_to(
376 provider: &OpenAIConfig,
377 uri: String,
378 body: &serde_json::Value,
379) -> Result<Encoded, EncodeError> {
380 let bytes = serde_json::to_vec(body)?;
381 let builder = http::Request::post(uri).header("Content-Type", "application/json");
382 encoded(provider, builder, Body::Bytes(bytes))
383}
384
385fn get(provider: &OpenAIConfig, path: &str) -> Result<Encoded, EncodeError> {
388 encoded(
389 provider,
390 http::Request::get(provider.uri(path, None)),
391 Body::empty(),
392 )
393}
394
395pub(crate) fn encoded(
399 provider: &OpenAIConfig,
400 builder: http::request::Builder,
401 body: Body,
402) -> Result<Encoded, EncodeError> {
403 let mut request = provider.authenticate(builder).body(body)?;
404 if let Some(envelope) = provider
405 .dialect
406 .quirks
407 .hooks
408 .and_then(|hooks| hooks.modality_envelope)
409 {
410 envelope(provider, &mut request)?;
411 }
412 Ok(Encoded::new(request, Framing::Whole)
413 .with_request_id_header(provider.dialect.request_id_header))
414}
415
416fn unsupported_parameter(provider: &str, parameter: &str) -> EncodeError {
418 EncodeError::request(format!(
419 "{provider} embeddings do not support the `{parameter}` parameter"
420 ))
421}
422
423#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
425pub struct Embeddings {
426 pub provider: OpenAIConfig,
428 pub model: String,
430 pub ndims: Option<usize>,
433 pub encoding_format: Option<EncodingFormat>,
435 pub user: Option<String>,
437}
438
439impl Embeddings {
440 pub fn new(provider: OpenAIConfig, model: impl Into<String>, ndims: Option<usize>) -> Self {
442 Self {
443 provider,
444 model: model.into(),
445 ndims,
446 encoding_format: None,
447 user: None,
448 }
449 }
450
451 pub fn with_encoding_format(mut self, encoding_format: EncodingFormat) -> Self {
453 self.encoding_format = Some(encoding_format);
454 self
455 }
456
457 pub fn with_user(mut self, user: impl Into<String>) -> Self {
459 self.user = Some(user.into());
460 self
461 }
462
463 fn model_width(&self) -> Option<&'static ModelWidth> {
466 self.provider
467 .dialect
468 .quirks
469 .embedding
470 .widths
471 .iter()
472 .find(|width| width.model == self.model)
473 }
474
475 fn resolved_ndims(&self) -> usize {
478 self.ndims
479 .or_else(|| self.model_width().and_then(|width| width.default))
480 .or_else(|| model_dimensions_from_identifier(&self.model))
481 .unwrap_or_default()
482 }
483
484 fn refuse_unhonourable_width(&self) -> Result<(), EncodeError> {
487 let quirks = &self.provider.dialect.quirks.embedding;
488 let provider = self.provider.dialect.name;
489 let invalid = |requirement, parameter| {
490 EncodeError::request(format!(
491 "{provider} embeddings require `{parameter}` {requirement}"
492 ))
493 };
494 let Some(parameter) = quirks.dimensions.name() else {
498 return Ok(());
499 };
500 let Some(declared) = self.ndims else {
501 return Ok(());
502 };
503 if declared == 0 {
504 return match quirks.refuse_zero_width {
505 Some(requirement) => Err(invalid(requirement, parameter)),
506 None => Ok(()),
507 };
508 }
509 let Some(width) = self.model_width() else {
512 return Ok(());
513 };
514 if width.default == Some(declared) {
516 return Ok(());
517 }
518 match width.accepted {
519 AcceptedWidths::Fixed => Err(unsupported_parameter(provider, parameter)),
520 AcceptedWidths::Range { min, max, .. } if (min..=max).contains(&declared) => Ok(()),
521 AcceptedWidths::Range { requirement, .. } => Err(invalid(requirement, parameter)),
522 }
523 }
524
525 fn requested_width(&self) -> Option<(&'static str, usize)> {
530 let field = self.provider.dialect.quirks.embedding.dimensions.name()?;
531 if self.model == crate::providers::openai::embedding::TEXT_EMBEDDING_ADA_002 {
532 return None;
533 }
534 let ndims = match self.resolved_ndims() {
536 0 => return None,
537 ndims => ndims,
538 };
539 if self
543 .model_width()
544 .is_some_and(|width| width.default == Some(ndims))
545 {
546 return None;
547 }
548 Some((field, ndims))
549 }
550}
551
552#[derive(Default)]
554pub struct EmbeddingsDecoder {
555 requires_usage: bool,
557 provider: &'static str,
558 model: String,
559}
560
561impl<'id> Decoder<'id, Embedding> for EmbeddingsDecoder {
562 type Event = CompatibleEmbeddingResponse;
563
564 fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
565 classify_untyped_line(frame.as_str().as_bytes())
566 }
567
568 fn decode(
569 &mut self,
570 event: Self::Event,
571 out: Out<'id, Embedding>,
572 ) -> Result<Flow, ProviderError> {
573 if event.usage.is_none() && self.requires_usage {
574 return Err(ProviderError::Response(format!(
575 "{} embedding response omitted required usage",
576 self.provider
577 )));
578 }
579 let usage = event
580 .usage
581 .as_ref()
582 .map(Usage::to_normalized)
583 .unwrap_or_default();
584 let vectors = event.data.into_iter().map(|datum| datum.embedding);
585 let model = if event.model.is_empty() {
586 self.model.clone()
587 } else {
588 event.model
589 };
590 Ok(out.end(embeddings::EmbeddingResponse {
591 model: Some(model),
592 usage,
593 ..embeddings::EmbeddingResponse::from_vectors(vectors)
594 }))
595 }
596}
597
598impl Wire for Embeddings {
599 type Op = Embedding;
600 type Payload = crate::wire::Encoded;
601 type Frame = crate::wire::WireFrame;
602 type Event = CompatibleEmbeddingResponse;
603 type Decoder<'id> = EmbeddingsDecoder;
604 type Reassembler = crate::wire::document::Unreassembled;
605
606 fn describe(&self) -> Descriptor<'_> {
608 Descriptor::new(self.provider.dialect.name)
609 .model(self.model.as_str())
610 .capabilities(
611 Capabilities::embedding(
612 self.provider.dialect.quirks.embedding.max_documents,
613 self.resolved_ndims(),
614 )
615 .declaring(self.ndims),
616 )
617 }
618
619 fn encode(&self, request: Vec<String>, _mode: Mode) -> Result<Encoded, EncodeError> {
620 let quirks = &self.provider.dialect.quirks.embedding;
621 if self.encoding_format == Some(EncodingFormat::Base64) {
624 return Err(EncodeError::request(format!(
625 "Rig cannot decode {} embedding responses encoded as `base64`",
626 self.provider.dialect.name
627 )));
628 }
629 if self.encoding_format.is_some() && !quirks.supports_encoding_format {
630 return Err(unsupported_parameter(
631 self.provider.dialect.name,
632 "encoding_format",
633 ));
634 }
635 if self.user.is_some() && !quirks.supports_user {
636 return Err(unsupported_parameter(self.provider.dialect.name, "user"));
637 }
638 self.refuse_unhonourable_width()?;
639
640 let mut body = serde_json::json!({ "input": request });
641 let Some(object) = body.as_object_mut() else {
642 return Err(EncodeError::request(
643 "embedding request body must be an object",
644 ));
645 };
646 if quirks.sends_model_field {
647 object.insert("model".to_owned(), serde_json::json!(self.model));
648 }
649 if let Some((field, ndims)) = self.requested_width() {
650 object.insert(field.to_owned(), serde_json::json!(ndims));
651 }
652 if let Some(encoding_format) = self.encoding_format {
653 object.insert(
654 "encoding_format".to_owned(),
655 serde_json::to_value(encoding_format)?,
656 );
657 }
658 if let Some(user) = &self.user {
659 object.insert("user".to_owned(), serde_json::json!(user));
660 }
661
662 json_post(
663 &self.provider,
664 self.provider.dialect.quirks.embeddings_path,
665 self.provider.deployment(&self.model),
666 &body,
667 )
668 }
669
670 fn decoder<'id>(&self) -> Self::Decoder<'id> {
671 EmbeddingsDecoder {
672 requires_usage: self.provider.dialect.quirks.embedding.requires_usage,
673 provider: self.provider.dialect.name,
674 model: self.model.clone(),
675 }
676 }
677}
678
679#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
681pub struct Transcriptions {
682 pub provider: OpenAIConfig,
684 pub model: String,
686}
687
688impl Transcriptions {
689 pub fn new(provider: OpenAIConfig, model: impl Into<String>) -> Self {
691 Self {
692 provider,
693 model: model.into(),
694 }
695 }
696
697 fn multipart_body(&self, request: TranscriptionRequest) -> Result<Body, EncodeError> {
700 use crate::http_client::MultipartForm;
701 use crate::http_client::multipart::Part;
702
703 let mut form = MultipartForm::new();
704 if self.provider.deployment(&self.model).is_none() {
706 form = form.text("model", self.model.clone());
707 }
708 form = form.part(Part::bytes("file", request.data).filename(request.filename));
709 if let Some(language) = request.language {
710 form = form.text("language", language);
711 }
712 if let Some(prompt) = request.prompt {
713 form = form.text("prompt", prompt);
714 }
715 if let Some(temperature) = request.temperature {
716 form = form.text("temperature", temperature.to_string());
717 }
718 if let Some(additional_params) = request.additional_params {
719 for (name, value) in additional_params_object(&additional_params)? {
720 match value {
721 serde_json::Value::Array(items) => {
725 for item in items {
726 form = form.text(format!("{name}[]"), form_text(item));
727 }
728 }
729 other => form = form.text(name.clone(), form_text(other)),
730 }
731 }
732 }
733 Ok(Body::Multipart(form))
734 }
735
736 fn input_audio_body(&self, request: TranscriptionRequest) -> Result<Body, EncodeError> {
739 use base64::Engine;
740
741 if request.prompt.is_some() {
742 return Err(EncodeError::request(std::io::Error::new(
743 std::io::ErrorKind::InvalidInput,
744 "OpenRouter STT does not support a top-level prompt field. \
745 Provider-specific prompt options can be passed via `additional_params`. \
746 Example: {\"provider\": {\"options\": {\"<provider>\": {\"prompt\": \"<text>\"}}}}",
747 )));
748 }
749
750 let mut body = serde_json::Map::new();
751 body.insert("model".to_owned(), serde_json::json!(self.model));
752 body.insert(
753 "input_audio".to_owned(),
754 serde_json::json!({
755 "data": base64::engine::general_purpose::STANDARD.encode(&request.data),
756 "format": audio_format_of(&request.filename),
757 }),
758 );
759 if let Some(language) = request.language {
760 body.insert("language".to_owned(), serde_json::json!(language));
761 }
762 if let Some(temperature) = request.temperature {
763 body.insert("temperature".to_owned(), serde_json::json!(temperature));
764 }
765 if let Some(additional_params) = request.additional_params {
766 for (name, value) in additional_params_object(&additional_params)? {
767 body.insert(name.clone(), value.clone());
768 }
769 }
770 Ok(Body::Bytes(serde_json::to_vec(
771 &serde_json::Value::Object(body),
772 )?))
773 }
774}
775
776fn form_text(value: &serde_json::Value) -> String {
779 match value {
780 serde_json::Value::String(value) => value.clone(),
781 other => other.to_string(),
782 }
783}
784
785fn additional_params_object(
787 params: &serde_json::Value,
788) -> Result<&serde_json::Map<String, serde_json::Value>, EncodeError> {
789 params.as_object().ok_or_else(|| {
790 EncodeError::request(std::io::Error::new(
791 std::io::ErrorKind::InvalidInput,
792 "additional transcription parameters must be a JSON object",
793 ))
794 })
795}
796
797fn audio_format_of(filename: &str) -> &'static str {
800 let extension = std::path::Path::new(filename)
801 .extension()
802 .and_then(std::ffi::OsStr::to_str)
803 .map(str::to_ascii_lowercase);
804 match extension.as_deref() {
805 Some("mp3") => "mp3",
806 Some("flac") => "flac",
807 Some("m4a") => "m4a",
808 Some("ogg") => "ogg",
809 Some("webm") => "webm",
810 Some("aac") => "aac",
811 _ => "wav",
812 }
813}
814
815impl Wire for Transcriptions {
816 type Op = Transcription;
817 type Payload = crate::wire::Encoded;
818 type Frame = crate::wire::WireFrame;
819 type Event = crate::providers::openai::transcription::TranscriptionResponse;
820 type Decoder<'id> = DocumentDecoder<Transcription, Self::Event>;
821 type Reassembler = crate::wire::document::Unreassembled;
822
823 fn describe(&self) -> Descriptor<'_> {
824 Descriptor::new(self.provider.dialect.name).model(self.model.as_str())
825 }
826
827 fn encode(&self, request: TranscriptionRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
828 let uri = self
829 .provider
830 .modality_uri(
831 "transcription",
832 self.provider.dialect.quirks.transcription_path,
833 &self.model,
834 )
835 .map_err(EncodeError::request)?;
836 let builder = http::Request::post(uri);
837 let (builder, body) = match self.provider.dialect.quirks.transcription_body {
838 TranscriptionBody::Multipart => (builder, self.multipart_body(request)?),
839 TranscriptionBody::InputAudioJson => (
840 builder.header(http::header::CONTENT_TYPE, "application/json"),
841 self.input_audio_body(request)?,
842 ),
843 };
844 encoded(&self.provider, builder, body)
845 }
846
847 fn decoder<'id>(&self) -> Self::Decoder<'id> {
848 DocumentDecoder::new(
849 |body| classify_untyped_line(body.as_bytes()),
850 |event| event.normalize(),
851 )
852 }
853}
854
855#[cfg(feature = "image")]
857#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
858pub struct Images {
859 pub provider: OpenAIConfig,
861 pub model: String,
863}
864
865#[cfg(feature = "image")]
866impl Images {
867 pub fn new(provider: OpenAIConfig, model: impl Into<String>) -> Self {
869 Self {
870 provider,
871 model: model.into(),
872 }
873 }
874}
875
876#[cfg(feature = "image")]
882#[derive(Default)]
883pub struct ImagesDecoder {
884 body: ImageBody,
886}
887
888#[cfg(feature = "image")]
890#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct ImageDatum {
892 pub b64_json: String,
894}
895
896#[cfg(feature = "image")]
898#[derive(Debug, Clone, Serialize, Deserialize)]
899#[serde(untagged)]
900pub enum ImagesReplyImage {
901 Keyed {
903 image: String,
905 },
906 Bare(String),
908}
909
910#[cfg(feature = "image")]
911impl ImagesReplyImage {
912 pub fn base64(&self) -> &str {
914 match self {
915 Self::Keyed { image } => image,
916 Self::Bare(image) => image,
917 }
918 }
919}
920
921#[cfg(feature = "image")]
923#[derive(Debug, Clone, Serialize, Deserialize)]
924pub struct ImagesReply {
925 #[serde(default)]
927 pub data: Vec<ImageDatum>,
928 #[serde(default)]
930 pub images: Vec<ImagesReplyImage>,
931 #[serde(flatten)]
935 pub extra: serde_json::Map<String, serde_json::Value>,
936}
937
938#[cfg(feature = "image")]
939impl ImagesReply {
940 pub fn first_base64(&self) -> Option<&str> {
942 self.data
943 .first()
944 .map(|image| image.b64_json.as_str())
945 .or_else(|| self.images.first().map(ImagesReplyImage::base64))
946 .filter(|encoded| !encoded.is_empty())
947 }
948}
949
950#[cfg(feature = "image")]
952#[derive(Debug, Clone)]
953pub enum ImagesEvent {
954 Json(ImagesReply),
956 Raw(Vec<u8>),
958 MiniMax(Vec<u8>),
960}
961
962#[cfg(feature = "image")]
963impl<'id> Decoder<'id, crate::operation::ImageGeneration> for ImagesDecoder {
964 type Event = ImagesEvent;
965
966 fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
967 match self.body {
968 ImageBody::HuggingFace => WireEvent::Known(ImagesEvent::Raw(match frame {
970 WireFrame::Text(text) => text.into_bytes(),
971 WireFrame::Bytes(bytes) => bytes,
972 })),
973 ImageBody::MiniMax => WireEvent::Known(ImagesEvent::MiniMax(match frame {
974 WireFrame::Text(text) => text.into_bytes(),
975 WireFrame::Bytes(bytes) => bytes,
976 })),
977 ImageBody::OpenAi | ImageBody::Xai | ImageBody::Hyperbolic | ImageBody::Venice => {
978 classify_untyped_line(frame.as_str().as_bytes()).map(ImagesEvent::Json)
979 }
980 }
981 }
982
983 fn decode(
984 &mut self,
985 event: Self::Event,
986 out: Out<'id, crate::operation::ImageGeneration>,
987 ) -> Result<Flow, ProviderError> {
988 use crate::image_generation::ImageGenerationResponse;
989 use base64::Engine;
990
991 let reply = match event {
992 ImagesEvent::Raw(image) => {
995 return Ok(out.end(ImageGenerationResponse::new(image)));
996 }
997 ImagesEvent::MiniMax(body) => {
998 let reply = crate::providers::minimax::decode_reply::<
999 crate::providers::minimax::image_generation::ImageGenerationReply,
1000 >(&body)?;
1001 return Ok(out.end(reply.into_response()?));
1002 }
1003 ImagesEvent::Json(reply) => reply,
1004 };
1005 let Some(encoded) = reply.first_base64() else {
1006 return Err(ProviderError::Response("missing image data".to_owned()));
1007 };
1008 let image = match base64::prelude::BASE64_STANDARD.decode(encoded) {
1009 Ok(image) => image,
1010 Err(error) => {
1011 return Err(ProviderError::Response(error.to_string()));
1012 }
1013 };
1014 Ok(out.end(ImageGenerationResponse::new(image)))
1015 }
1016}
1017
1018#[cfg(feature = "image")]
1019impl Wire for Images {
1020 type Op = crate::operation::ImageGeneration;
1021 type Payload = crate::wire::Encoded;
1022 type Frame = crate::wire::WireFrame;
1023 type Event = ImagesEvent;
1024 type Decoder<'id> = ImagesDecoder;
1025 type Reassembler = crate::wire::document::Unreassembled;
1026
1027 fn describe(&self) -> Descriptor<'_> {
1028 Descriptor::new(self.provider.dialect.name).model(self.model.as_str())
1029 }
1030
1031 fn encode(
1032 &self,
1033 request: crate::image_generation::ImageGenerationRequest,
1034 _mode: Mode,
1035 ) -> Result<Encoded, EncodeError> {
1036 if !request.images.is_empty() {
1037 return Err(EncodeError::request(
1038 "this image generation endpoint takes no reference images",
1039 ));
1040 }
1041 let mut body = match self.provider.dialect.quirks.image_body {
1042 ImageBody::OpenAi => serde_json::json!({
1047 "model": self.model,
1048 "prompt": request.prompt,
1049 "size": format!("{}x{}", request.width, request.height),
1050 }),
1051 ImageBody::Xai => serde_json::json!({
1054 "model": self.model,
1055 "prompt": request.prompt,
1056 "response_format": "b64_json",
1057 "aspect_ratio": "1:1",
1058 }),
1059 ImageBody::Hyperbolic => serde_json::json!({
1060 "model_name": self.model,
1061 "prompt": request.prompt,
1062 "height": request.height,
1063 "width": request.width,
1064 }),
1065 ImageBody::Venice => serde_json::json!({
1066 "model": self.model,
1067 "prompt": request.prompt,
1068 "width": request.width,
1069 "height": request.height,
1070 }),
1071 ImageBody::MiniMax => {
1072 crate::providers::minimax::image_generation::request_body(&self.model, &request)?
1073 }
1074 ImageBody::HuggingFace => serde_json::json!({
1076 "inputs": request.prompt,
1077 "parameters": {
1078 "width": request.width,
1079 "height": request.height,
1080 },
1081 }),
1082 };
1083 if let Some(additional_params) = request.additional_params {
1086 crate::json_utils::merge_inplace(&mut body, additional_params);
1087 }
1088
1089 let uri = self
1090 .provider
1091 .modality_uri(
1092 "image generation",
1093 self.provider.dialect.quirks.image_generation_path,
1094 &self.model,
1095 )
1096 .map_err(EncodeError::request)?;
1097 json_post_to(&self.provider, uri, &body)
1098 }
1099
1100 fn decoder<'id>(&self) -> Self::Decoder<'id> {
1101 ImagesDecoder {
1102 body: self.provider.dialect.quirks.image_body,
1103 }
1104 }
1105}
1106
1107#[cfg(feature = "audio")]
1109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1110pub struct Speech {
1111 pub provider: OpenAIConfig,
1113 pub model: String,
1115}
1116
1117#[cfg(feature = "audio")]
1118impl Speech {
1119 pub fn new(provider: OpenAIConfig, model: impl Into<String>) -> Self {
1121 Self {
1122 provider,
1123 model: model.into(),
1124 }
1125 }
1126}
1127
1128#[cfg(feature = "audio")]
1130#[derive(Default)]
1131pub struct SpeechDecoder {
1132 body: SpeechBody,
1134}
1135
1136#[cfg(feature = "audio")]
1139#[derive(Debug, Clone, Serialize, Deserialize)]
1140pub struct SpeechReply {
1141 pub audio: String,
1143}
1144
1145#[cfg(feature = "audio")]
1146impl<'id> Decoder<'id, crate::operation::AudioGeneration> for SpeechDecoder {
1147 type Event = Vec<u8>;
1148
1149 fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
1150 WireEvent::Known(match frame {
1152 WireFrame::Text(text) => text.into_bytes(),
1153 WireFrame::Bytes(bytes) => bytes,
1154 })
1155 }
1156
1157 fn decode(
1158 &mut self,
1159 event: Self::Event,
1160 out: Out<'id, crate::operation::AudioGeneration>,
1161 ) -> Result<Flow, ProviderError> {
1162 use base64::Engine;
1163
1164 let audio = match self.body {
1165 SpeechBody::OpenAi | SpeechBody::Xai => event,
1167 SpeechBody::MiniMax => {
1168 let reply = crate::providers::minimax::decode_reply::<
1169 crate::providers::minimax::audio_generation::SpeechReply,
1170 >(&event)?;
1171 return Ok(out.end(reply.into_response()?));
1172 }
1173 SpeechBody::Hyperbolic => {
1175 let reply = match serde_json::from_slice::<SpeechReply>(&event) {
1176 Ok(reply) => reply,
1177 Err(error) => {
1178 return Err(ProviderError::Response(error.to_string()));
1179 }
1180 };
1181 match base64::prelude::BASE64_STANDARD.decode(&reply.audio) {
1182 Ok(audio) => audio,
1183 Err(error) => {
1184 return Err(ProviderError::Response(error.to_string()));
1185 }
1186 }
1187 }
1188 };
1189 Ok(out.end(crate::audio_generation::AudioGenerationResponse::new(audio)))
1190 }
1191}
1192
1193#[cfg(feature = "audio")]
1194impl Wire for Speech {
1195 type Op = crate::operation::AudioGeneration;
1196 type Payload = crate::wire::Encoded;
1197 type Frame = crate::wire::WireFrame;
1198 type Event = Vec<u8>;
1199 type Decoder<'id> = SpeechDecoder;
1200 type Reassembler = crate::wire::document::Unreassembled;
1201
1202 fn describe(&self) -> Descriptor<'_> {
1203 Descriptor::new(self.provider.dialect.name).model(self.model.as_str())
1204 }
1205
1206 fn encode(
1207 &self,
1208 request: crate::audio_generation::AudioGenerationRequest,
1209 _mode: Mode,
1210 ) -> Result<Encoded, EncodeError> {
1211 let mut additional_params = request.additional_params;
1212 let mut body = match self.provider.dialect.quirks.speech_body {
1213 SpeechBody::OpenAi => serde_json::json!({
1214 "model": self.model,
1215 "input": request.text,
1216 "voice": request.voice,
1217 "speed": request.speed,
1218 }),
1219 SpeechBody::Xai => serde_json::json!({
1221 "text": request.text,
1222 "voice_id": if request.voice.is_empty() { "eve" } else { request.voice.as_str() },
1223 "language": "en",
1224 }),
1225 SpeechBody::Hyperbolic => serde_json::json!({
1228 "language": self.model,
1229 "speaker": request.voice,
1230 "text": request.text,
1231 "speed": request.speed,
1232 }),
1233 SpeechBody::MiniMax => serde_json::json!({
1235 "model": self.model,
1236 "text": request.text,
1237 "stream": false,
1238 "output_format": "hex",
1239 "voice_setting": {
1240 "voice_id": request.voice,
1241 "speed": request.speed,
1242 },
1243 }),
1244 };
1245 if let Some(derived) = body
1248 .get_mut("voice_setting")
1249 .and_then(serde_json::Value::as_object_mut)
1250 && let Some(params) = additional_params
1251 .as_mut()
1252 .and_then(serde_json::Value::as_object_mut)
1253 && let Some(serde_json::Value::Object(caller)) = params.get("voice_setting")
1254 {
1255 derived.extend(caller.clone());
1256 params.shift_remove("voice_setting");
1257 }
1258 if let Some(additional_params) = additional_params {
1260 crate::json_utils::merge_inplace(&mut body, additional_params);
1261 }
1262
1263 let uri = self.provider.uri_versioned(
1266 self.provider.dialect.quirks.audio_generation_path,
1267 self.provider.deployment(&self.model),
1268 self.provider.speech_api_version(),
1269 );
1270 json_post_to(&self.provider, uri, &body)
1271 }
1272
1273 fn decoder<'id>(&self) -> Self::Decoder<'id> {
1274 SpeechDecoder {
1275 body: self.provider.dialect.quirks.speech_body,
1276 }
1277 }
1278}
1279
1280#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1282pub struct Models {
1283 pub provider: OpenAIConfig,
1285}
1286
1287impl Models {
1288 pub fn new(provider: OpenAIConfig) -> Self {
1290 Self { provider }
1291 }
1292}
1293
1294#[derive(Debug, Deserialize)]
1298pub struct ModelEntry {
1299 pub id: String,
1300 #[serde(default)]
1301 pub name: Option<String>,
1302 #[serde(default)]
1303 pub description: Option<String>,
1304 #[serde(default, rename = "type")]
1306 pub kind: Option<String>,
1307 #[serde(default)]
1308 pub created: Option<u64>,
1309 #[serde(default)]
1310 pub owned_by: Option<String>,
1311 #[serde(default)]
1312 pub context_window: Option<u32>,
1313 #[serde(default)]
1314 pub context_length: Option<u32>,
1315 #[serde(default)]
1316 pub max_context_length: Option<u32>,
1317 #[serde(default)]
1318 pub max_completion_tokens: Option<u32>,
1319 #[serde(default)]
1320 pub top_provider: Option<TopProvider>,
1321}
1322
1323#[derive(Debug, Deserialize)]
1326pub struct TopProvider {
1327 #[serde(default)]
1328 pub max_completion_tokens: Option<u32>,
1329}
1330
1331impl From<ModelEntry> for ModelInfo {
1332 fn from(entry: ModelEntry) -> Self {
1333 let mut model = ModelInfo::from_id(entry.id);
1334 model.name = entry.name;
1335 model.description = entry.description;
1336 model.r#type = entry.kind;
1337 model.created_at = entry.created;
1338 model.owned_by = entry.owned_by;
1339 model.context_length = entry
1340 .context_window
1341 .or(entry.context_length)
1342 .or(entry.max_context_length);
1343 model.max_output_tokens = entry.max_completion_tokens.or_else(|| {
1344 entry
1345 .top_provider
1346 .and_then(|provider| provider.max_completion_tokens)
1347 });
1348 model
1349 }
1350}
1351
1352#[derive(Debug, Deserialize)]
1354pub struct ModelsReply {
1355 #[serde(default)]
1356 pub data: Vec<ModelEntry>,
1357}
1358
1359impl Wire for Models {
1360 type Op = ModelListing;
1361 type Payload = crate::wire::Encoded;
1362 type Frame = crate::wire::WireFrame;
1363 type Event = ModelsReply;
1364 type Decoder<'id> = DocumentDecoder<ModelListing, Self::Event>;
1365 type Reassembler = crate::wire::document::Unreassembled;
1366
1367 fn describe(&self) -> Descriptor<'_> {
1368 Descriptor::new(self.provider.dialect.name)
1369 }
1370
1371 fn encode(&self, _cursor: Option<String>, _mode: Mode) -> Result<Encoded, EncodeError> {
1373 get(&self.provider, self.provider.dialect.quirks.models_path)
1374 }
1375
1376 fn decoder<'id>(&self) -> Self::Decoder<'id> {
1378 DocumentDecoder::new(
1379 |body| classify_untyped_line(body.as_bytes()),
1380 |event| {
1381 let models = event.data.into_iter().map(ModelInfo::from).collect();
1382 Ok(ModelPage {
1383 models: ModelList::new(models),
1384 next: None,
1385 })
1386 },
1387 )
1388 }
1389}
1390
1391#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1394pub struct Rerank {
1395 pub provider: OpenAIConfig,
1397 pub model: String,
1399 pub top_n: Option<usize>,
1402}
1403
1404impl Rerank {
1405 pub fn new(provider: OpenAIConfig, model: impl Into<String>) -> Self {
1407 Self {
1408 provider,
1409 model: model.into(),
1410 top_n: None,
1411 }
1412 }
1413
1414 pub fn with_top_n(mut self, top_n: usize) -> Self {
1416 self.top_n = Some(top_n);
1417 self
1418 }
1419}
1420
1421#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub struct RerankResultEntry {
1424 pub index: usize,
1426 #[serde(alias = "score")]
1428 pub relevance_score: f64,
1429 #[serde(default, alias = "text")]
1432 pub document: Option<String>,
1433}
1434
1435#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1437pub struct RerankUsage {
1438 #[serde(default)]
1440 pub prompt_tokens: u64,
1441 #[serde(default)]
1443 pub total_tokens: u64,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize)]
1448pub struct RerankReply {
1449 #[serde(default)]
1451 pub model: Option<String>,
1452 pub results: Vec<RerankResultEntry>,
1454 #[serde(default)]
1456 pub usage: Option<RerankUsage>,
1457}
1458
1459impl Wire for Rerank {
1460 type Op = RerankOp;
1461 type Payload = crate::wire::Encoded;
1462 type Frame = crate::wire::WireFrame;
1463 type Event = RerankReply;
1464 type Decoder<'id> = DocumentDecoder<RerankOp, Self::Event>;
1465 type Reassembler = crate::wire::document::Unreassembled;
1466
1467 fn describe(&self) -> Descriptor<'_> {
1468 Descriptor::new(self.provider.dialect.name)
1469 .model(self.model.as_str())
1470 .capabilities(Capabilities::rerank(
1471 self.provider.dialect.quirks.rerank.max_documents,
1472 ))
1473 }
1474
1475 fn encode(
1476 &self,
1477 request: crate::operation::RerankRequest,
1478 _mode: Mode,
1479 ) -> Result<Encoded, EncodeError> {
1480 let quirks = &self.provider.dialect.quirks.rerank;
1481 if quirks.path.is_empty() {
1483 return Err(EncodeError::request(format!(
1484 "{} offers no reranking endpoint",
1485 self.provider.dialect.name
1486 )));
1487 }
1488 let mut body = serde_json::json!({
1489 "query": request.query,
1490 "documents": request.documents,
1491 });
1492 let Some(object) = body.as_object_mut() else {
1493 return Err(EncodeError::request(
1494 "rerank request body must be an object",
1495 ));
1496 };
1497 if quirks.sends_model_field {
1498 object.insert("model".to_owned(), serde_json::json!(self.model));
1499 }
1500 if let Some(top_n) = self.top_n {
1501 object.insert("top_n".to_owned(), serde_json::json!(top_n));
1502 }
1503
1504 json_post(
1505 &self.provider,
1506 quirks.path,
1507 self.provider.deployment(&self.model),
1508 &body,
1509 )
1510 }
1511
1512 fn decoder<'id>(&self) -> Self::Decoder<'id> {
1513 DocumentDecoder::new(
1514 |body| classify_untyped_line(body.as_bytes()),
1515 |event| {
1516 let usage = event
1517 .usage
1518 .map(|usage| crate::completion::Usage {
1519 input_tokens: Some(usage.prompt_tokens),
1520 total_tokens: Some(usage.total_tokens),
1521 ..Default::default()
1522 })
1523 .unwrap_or_default();
1524 let results = event
1525 .results
1526 .into_iter()
1527 .map(|result| crate::rerank::RerankResult {
1528 index: result.index,
1529 document: result.document,
1530 relevance_score: result.relevance_score,
1531 })
1532 .collect();
1533 Ok(crate::rerank::RerankResponse {
1534 model: event.model,
1535 usage,
1536 ..crate::rerank::RerankResponse::new(results)
1537 })
1538 },
1539 )
1540 }
1541}
1542
1543#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1545pub struct Verify {
1546 pub provider: OpenAIConfig,
1548}
1549
1550impl Verify {
1551 pub fn new(provider: OpenAIConfig) -> Self {
1553 Self { provider }
1554 }
1555}
1556
1557pub use crate::operation::VerifyDecoder;
1558
1559impl Wire for Verify {
1560 type Op = VerifyOp;
1561 type Payload = crate::wire::Encoded;
1562 type Frame = crate::wire::WireFrame;
1563 type Event = ();
1564 type Decoder<'id> = VerifyDecoder;
1565 type Reassembler = crate::wire::document::Unreassembled;
1566
1567 fn describe(&self) -> Descriptor<'_> {
1568 Descriptor::new(self.provider.dialect.name)
1569 }
1570
1571 fn encode(&self, _request: (), _mode: Mode) -> Result<Encoded, EncodeError> {
1572 let path = self.provider.dialect.quirks.verify_path;
1573 if path.is_empty() {
1574 return Err(EncodeError::request(format!(
1575 "{} offers no endpoint that checks a credential without consuming tokens",
1576 self.provider.dialect.name
1577 )));
1578 }
1579 get(&self.provider, path)
1580 }
1581
1582 fn decoder<'id>(&self) -> Self::Decoder<'id> {
1583 VerifyDecoder
1584 }
1585}
1586
1587#[cfg(test)]
1588mod tests;