1use std::borrow::Cow;
18use std::fmt;
19use std::hash::{Hash, Hasher};
20
21use serde::de;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24use crate::catalog::{Catalog, ModelFacts, ModelSpec, NotFound, Reference};
25use crate::client::env::{self, EnvError};
26use crate::driver::DynModel;
27use crate::http_client::{DynHttpClient, HttpClientExt};
28use crate::operation::Completion;
29use crate::providers::{anthropic, gemini, openai};
30use crate::serve::ErasedHandler;
31use crate::serve::adapters::ModelAdapter;
32use crate::wire::Secret;
33
34pub(crate) const OPENAI_DIALECTS: &[&openai::wire::Dialect] = &[
41 &openai::wire::OPENAI,
42 &openai::wire::AZURE,
43 &openai::wire::DEEPSEEK,
44 &openai::wire::GROQ,
45 &openai::wire::HYPERBOLIC,
46 &openai::wire::MIRA,
47 &openai::wire::PERPLEXITY,
48 &openai::wire::TOGETHER,
49 &openai::wire::HUGGINGFACE,
50 &openai::wire::LLAMACPP,
51 &openai::wire::MISTRAL,
52 &openai::wire::OPENROUTER,
53 &openai::wire::VENICE,
54 &openai::wire::DOUBLEWORD,
55 &openai::wire::ZAI,
56 &openai::wire::MINIMAX,
57 &openai::wire::MOONSHOT,
58 &openai::wire::XIAOMIMIMO,
59 &openai::wire::COHERE,
60 &openai::wire::OLLAMA,
61 &openai::wire::OPENCODE,
62 &openai::wire::OPENCODE_GO,
63 &crate::providers::xai::DIALECT,
64 &crate::providers::chatgpt::DIALECT,
65 &crate::providers::copilot::wire::DIALECT,
66];
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
72pub enum Format {
73 OpenAi,
75 Anthropic,
77 Gemini,
79}
80
81impl Format {
82 pub const ALL: [Format; 3] = [Format::OpenAi, Format::Anthropic, Format::Gemini];
84
85 pub fn as_str(self) -> &'static str {
87 match self {
88 Format::OpenAi => "openai",
89 Format::Anthropic => "anthropic",
90 Format::Gemini => "gemini",
91 }
92 }
93
94 pub fn named(name: &str) -> Option<Self> {
96 Self::ALL.into_iter().find(|format| format.as_str() == name)
97 }
98
99 fn names() -> String {
101 Self::ALL
102 .iter()
103 .map(|format| format.as_str())
104 .collect::<Vec<_>>()
105 .join(", ")
106 }
107}
108
109impl fmt::Display for Format {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.write_str(self.as_str())
112 }
113}
114
115impl Serialize for Format {
116 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
117 serializer.serialize_str(self.as_str())
118 }
119}
120
121impl<'de> Deserialize<'de> for Format {
122 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
123 let name = String::deserialize(deserializer)?;
124 Self::named(&name).ok_or_else(|| {
125 de::Error::custom(format!(
126 "`{name}` is not a protocol family ({})",
127 Format::names()
128 ))
129 })
130 }
131}
132
133#[derive(Debug, Clone, Copy)]
135enum Registered {
136 OpenAi(openai::wire::Dialect),
137 Anthropic(anthropic::wire::Dialect),
138 Gemini,
139}
140
141impl PartialEq for Registered {
143 fn eq(&self, other: &Self) -> bool {
144 self.vendor() == other.vendor() && self.format() == other.format()
145 }
146}
147
148impl Registered {
149 fn vendor(&self) -> &'static str {
150 match self {
151 Self::OpenAi(dialect) => dialect.name,
152 Self::Anthropic(dialect) => dialect.name,
153 Self::Gemini => gemini::PROVIDER_NAME,
154 }
155 }
156
157 fn format(&self) -> Format {
158 match self {
159 Self::OpenAi(_) => Format::OpenAi,
160 Self::Anthropic(_) => Format::Anthropic,
161 Self::Gemini => Format::Gemini,
162 }
163 }
164
165 fn config(&self, api_key: impl Into<Secret>) -> ProviderConfig {
166 match self {
167 Self::OpenAi(dialect) => {
168 ProviderConfig::OpenAi(openai::wire::OpenAIConfig::with_key(dialect, api_key))
169 }
170 Self::Anthropic(dialect) => ProviderConfig::Anthropic(
171 anthropic::wire::AnthropicConfig::with_key(dialect, api_key),
172 ),
173 Self::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::new(api_key)),
174 }
175 }
176
177 fn config_from_env(&self) -> Result<ProviderConfig, ConnectError> {
182 let tried = self.key_envs();
183 let (api_key, name) = key_from_env(self.vendor(), tried, self.requires_credential())?;
184 Ok(match self {
185 Self::OpenAi(dialect) => {
186 let auth = openai_auth(dialect, name);
187 ProviderConfig::OpenAi(openai::wire::OpenAIConfig::from_env_with_credential(
188 dialect, api_key, auth,
189 )?)
190 }
191 Self::Anthropic(dialect) => ProviderConfig::Anthropic(
192 anthropic::wire::AnthropicConfig::from_env_with_credential(dialect, api_key)?,
193 ),
194 Self::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::new(api_key)),
195 })
196 }
197
198 fn key_envs(&self) -> Vec<&'static str> {
200 match self {
201 Self::OpenAi(dialect) => openai_key_envs(dialect),
202 Self::Anthropic(dialect) => vec![dialect.api_key_env],
203 Self::Gemini => vec![gemini::API_KEY_ENV],
204 }
205 }
206
207 fn short_lived_key(&self) -> bool {
209 matches!(self, Self::OpenAi(dialect) if dialect.quirks.short_lived_key)
210 }
211
212 fn catalog_priced(&self) -> bool {
214 match self {
215 Self::OpenAi(dialect) => dialect.quirks.catalog_priced,
216 Self::Anthropic(dialect) => dialect.quirks.catalog_priced,
217 Self::Gemini => true,
218 }
219 }
220
221 fn requires_credential(&self) -> bool {
223 match self {
224 Self::OpenAi(dialect) => openai_requires_credential(dialect),
225 Self::Anthropic(dialect) => {
226 dialect.quirks.auth != anthropic::wire::Auth::OptionalBearer
227 }
228 Self::Gemini => true,
229 }
230 }
231}
232
233fn openai_key_envs(dialect: &openai::wire::Dialect) -> Vec<&'static str> {
236 std::iter::once(dialect.api_key_env)
237 .chain(
238 dialect
239 .alternate_auth
240 .map(|alternative| alternative.api_key_env),
241 )
242 .collect()
243}
244
245fn openai_requires_credential(dialect: &openai::wire::Dialect) -> bool {
248 !matches!(dialect.quirks.auth, openai::wire::Auth::OptionalBearer)
249}
250
251fn openai_auth(dialect: &openai::wire::Dialect, name: Option<&'static str>) -> openai::wire::Auth {
255 match dialect.alternate_auth {
256 Some(alternative) if name == Some(alternative.api_key_env) => alternative.auth,
257 _ => dialect.quirks.auth,
258 }
259}
260
261fn key_from_env(
265 vendor: &str,
266 tried: Vec<&'static str>,
267 required: bool,
268) -> Result<(String, Option<&'static str>), ConnectError> {
269 for name in &tried {
270 if let Some(value) = env::optional(name)?.filter(|value| !value.is_empty()) {
271 return Ok((value, Some(*name)));
272 }
273 }
274 if required {
275 return Err(ConnectError::MissingKey {
276 vendor: vendor.to_owned(),
277 tried: tried.into_iter().map(Cow::Borrowed).collect(),
278 });
279 }
280 Ok((String::new(), None))
281}
282
283#[derive(Debug)]
287struct CatalogOnly {
288 vendor: &'static str,
290 home: &'static str,
292 credential: bool,
294}
295
296static CATALOG_ONLY: [CatalogOnly; 5] = [
298 CatalogOnly {
299 vendor: "aws_bedrock",
300 home: "the `rig-bedrock` crate",
301 credential: true,
302 },
303 CatalogOnly {
304 vendor: "vertexai",
305 home: "the `rig-vertexai` crate",
306 credential: true,
307 },
308 CatalogOnly {
309 vendor: "gemini-grpc",
310 home: "the `rig-gemini-grpc` crate",
311 credential: true,
312 },
313 CatalogOnly {
314 vendor: "candle",
315 home: "the `rig-candle` crate",
316 credential: false,
317 },
318 CatalogOnly {
319 vendor: "voyageai",
320 home: "`rig_core::providers::voyageai`, which serves embeddings and reranking only",
321 credential: true,
322 },
323];
324
325#[derive(Debug, Clone, Copy)]
327enum Kind {
328 Registered(Registered),
330 CatalogOnly(&'static CatalogOnly),
332 Declared(&'static str),
334}
335
336static DECLARED: std::sync::RwLock<Vec<&'static str>> = std::sync::RwLock::new(Vec::new());
340
341fn declared(vendor: &str) -> Option<&'static str> {
343 DECLARED
344 .read()
345 .unwrap_or_else(std::sync::PoisonError::into_inner)
346 .iter()
347 .copied()
348 .find(|name| *name == vendor)
349}
350
351#[derive(Debug, Clone, Copy)]
360pub struct ProviderId(Kind);
361
362impl PartialEq for ProviderId {
365 fn eq(&self, other: &Self) -> bool {
366 self.vendor() == other.vendor() && self.format() == other.format()
367 }
368}
369
370impl Eq for ProviderId {}
371
372impl Hash for ProviderId {
373 fn hash<H: Hasher>(&self, state: &mut H) {
374 self.vendor().hash(state);
375 self.format().hash(state);
376 }
377}
378
379impl ProviderId {
380 pub fn all() -> impl Iterator<Item = ProviderId> {
384 openai::wire::all()
385 .map(|dialect| Registered::OpenAi(*dialect))
386 .chain(anthropic::wire::all().map(|dialect| Registered::Anthropic(*dialect)))
387 .chain(std::iter::once(Registered::Gemini))
388 .map(|registered| ProviderId(Kind::Registered(registered)))
389 }
390
391 pub fn new(vendor: &str, format: Format) -> Option<Self> {
394 let registered = match format {
395 Format::OpenAi => {
396 openai::wire::by_name(vendor).map(|dialect| Registered::OpenAi(*dialect))
397 }
398 Format::Anthropic => {
399 anthropic::wire::Dialect::by_name(vendor).map(Registered::Anthropic)
400 }
401 Format::Gemini => (vendor == gemini::PROVIDER_NAME).then_some(Registered::Gemini),
402 };
403 registered.map(|registered| Self(Kind::Registered(registered)))
404 }
405
406 pub fn catalog(vendor: &str) -> Option<Self> {
418 let primary = primary(vendor).and_then(|format| Self::new(vendor, format));
419 primary
420 .or_else(|| Self::vendor_selections(vendor).next())
421 .or_else(|| {
422 CATALOG_ONLY
423 .iter()
424 .find(|provider| provider.vendor == vendor)
425 .map(|provider| Self(Kind::CatalogOnly(provider)))
426 })
427 .or_else(|| declared(vendor).map(|name| Self(Kind::Declared(name))))
428 }
429
430 pub fn declare(vendor: &str) -> Result<Self, SelectionError> {
460 if vendor.is_empty() || vendor.contains('/') || vendor.contains(char::is_whitespace) {
461 return Err(SelectionError::Malformed {
462 selection: vendor.to_owned(),
463 });
464 }
465 if let Some(known) = Self::catalog(vendor) {
466 return Ok(known);
467 }
468 let mut names = DECLARED
469 .write()
470 .unwrap_or_else(std::sync::PoisonError::into_inner);
471 let name = match names.iter().copied().find(|name| *name == vendor) {
472 Some(name) => name,
473 None => {
474 let name: &'static str = Box::leak(vendor.to_owned().into_boxed_str());
475 names.push(name);
476 name
477 }
478 };
479 Ok(Self(Kind::Declared(name)))
480 }
481
482 pub(crate) fn catalog_vendors() -> impl Iterator<Item = &'static str> {
485 let declared: Vec<&'static str> = DECLARED
486 .read()
487 .unwrap_or_else(std::sync::PoisonError::into_inner)
488 .clone();
489 Self::all()
490 .map(|id| id.vendor())
491 .chain(CATALOG_ONLY.iter().map(|provider| provider.vendor))
492 .chain(declared)
493 }
494
495 pub fn vendor(&self) -> &'static str {
497 match &self.0 {
498 Kind::Registered(registered) => registered.vendor(),
499 Kind::CatalogOnly(provider) => provider.vendor,
500 Kind::Declared(vendor) => vendor,
501 }
502 }
503
504 pub fn format(&self) -> Option<Format> {
507 match &self.0 {
508 Kind::Registered(registered) => Some(registered.format()),
509 Kind::CatalogOnly(_) | Kind::Declared(_) => None,
510 }
511 }
512
513 pub fn is_registered(&self) -> bool {
517 matches!(self.0, Kind::Registered(_))
518 }
519
520 pub fn is_declared(&self) -> bool {
522 matches!(self.0, Kind::Declared(_))
523 }
524
525 pub fn vendor_selections(vendor: &str) -> impl Iterator<Item = ProviderId> + '_ {
527 Self::all().filter(move |id| id.vendor() == vendor)
528 }
529
530 pub fn resolve(selection: &str) -> Result<Self, SelectionError> {
537 let malformed = || SelectionError::Malformed {
538 selection: selection.to_owned(),
539 };
540 let (vendor, format) = match selection.split_once('/') {
541 Some((_, rest)) if rest.contains('/') => return Err(malformed()),
542 Some((vendor, format)) => (vendor, Some(format)),
543 None => (selection, None),
544 };
545 if vendor.is_empty() {
546 return Err(malformed());
547 }
548 let Some(format) = format else {
549 let mut registered = Self::vendor_selections(vendor);
550 let first = registered.next().ok_or_else(|| SelectionError::Unknown {
551 vendor: vendor.to_owned(),
552 })?;
553 return match registered.next() {
554 None => Ok(first),
555 Some(_) => Err(SelectionError::Ambiguous {
556 vendor: vendor.to_owned(),
557 alternatives: alternatives(vendor),
558 }),
559 };
560 };
561 let Some(family) = Format::named(format) else {
562 return Err(SelectionError::UnknownFormat {
563 format: format.to_owned(),
564 });
565 };
566 Self::selection(vendor, family)
567 }
568
569 pub fn config(&self, api_key: impl Into<Secret>) -> Option<ProviderConfig> {
573 match &self.0 {
574 Kind::Registered(registered) => Some(registered.config(api_key)),
575 Kind::CatalogOnly(_) | Kind::Declared(_) => None,
576 }
577 }
578
579 pub fn api_key_envs(&self) -> Vec<&'static str> {
585 match &self.0 {
586 Kind::Registered(registered) => registered.key_envs(),
587 Kind::CatalogOnly(_) | Kind::Declared(_) => Vec::new(),
588 }
589 }
590
591 pub fn requires_credential(&self) -> bool {
597 match &self.0 {
598 Kind::Registered(registered) => registered.requires_credential(),
599 Kind::CatalogOnly(provider) => provider.credential,
600 Kind::Declared(_) => false,
601 }
602 }
603
604 pub fn short_lived_key(&self) -> bool {
607 matches!(&self.0, Kind::Registered(registered) if registered.short_lived_key())
608 }
609
610 pub(crate) fn catalog_priced(&self) -> bool {
613 match &self.0 {
614 Kind::Registered(registered) => registered.catalog_priced(),
615 Kind::CatalogOnly(_) | Kind::Declared(_) => true,
616 }
617 }
618
619 fn registered(self) -> Result<Registered, ConnectError> {
621 match self.0 {
622 Kind::Registered(registered) => Ok(registered),
623 Kind::CatalogOnly(provider) => Err(ConnectError::CatalogOnly {
624 vendor: provider.vendor.to_owned(),
625 served_by: provider.home,
626 }),
627 Kind::Declared(vendor) => Err(ConnectError::CatalogOnly {
628 vendor: vendor.to_owned(),
629 served_by: "whoever declared it: provide it with `Connector::provide`",
630 }),
631 }
632 }
633
634 fn selection(vendor: &str, format: Format) -> Result<Self, SelectionError> {
636 Self::new(vendor, format).ok_or_else(|| {
637 let alternatives = alternatives(vendor);
638 match alternatives.is_empty() {
639 true => SelectionError::Unknown {
640 vendor: vendor.to_owned(),
641 },
642 false => SelectionError::Unregistered {
643 vendor: vendor.to_owned(),
644 format,
645 alternatives,
646 },
647 }
648 })
649 }
650}
651
652fn primary(vendor: &str) -> Option<Format> {
655 match vendor {
656 "minimax" => Some(Format::Anthropic),
657 "zai" | "moonshot" | "xiaomimimo" | "ollama" | "opencode" | "opencode-go" => {
658 Some(Format::OpenAi)
659 }
660 _ => None,
661 }
662}
663
664fn alternatives(vendor: &str) -> Vec<String> {
666 ProviderId::vendor_selections(vendor)
667 .map(|id| id.to_string())
668 .collect()
669}
670
671impl fmt::Display for ProviderId {
672 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
673 match self.format() {
674 Some(format) => write!(f, "{}/{format}", self.vendor()),
675 None => f.write_str(self.vendor()),
676 }
677 }
678}
679
680impl Serialize for ProviderId {
681 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
682 serializer.collect_str(self)
683 }
684}
685
686impl<'de> Deserialize<'de> for ProviderId {
690 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
691 let selection = String::deserialize(deserializer)?;
692 Self::resolve(&selection)
693 .or_else(|error| {
694 Self::catalog(&selection)
695 .filter(|id| !id.is_registered())
696 .ok_or(error)
697 })
698 .map_err(de::Error::custom)
699 }
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
704pub enum SelectionError {
705 #[error("no registered provider is named `{vendor}`")]
707 Unknown {
708 vendor: String,
710 },
711 #[error(
713 "`{vendor}` speaks no {format} endpoint in this build (it speaks {})",
714 alternatives.join(", ")
715 )]
716 Unregistered {
717 vendor: String,
719 format: Format,
721 alternatives: Vec<String>,
723 },
724 #[error(
726 "`{vendor}` names more than one registered selection: name one of {}",
727 alternatives.join(", ")
728 )]
729 Ambiguous {
730 vendor: String,
732 alternatives: Vec<String>,
734 },
735 #[error("`{format}` is not a protocol family ({})", Format::names())]
737 UnknownFormat {
738 format: String,
740 },
741 #[error("`{selection}` is not a provider selection: expected `vendor` or `vendor/format`")]
743 Malformed {
744 selection: String,
746 },
747}
748
749#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
754pub enum ProviderConfig {
755 #[serde(rename = "openai")]
757 OpenAi(openai::wire::OpenAIConfig),
758 #[serde(rename = "anthropic")]
760 Anthropic(anthropic::wire::AnthropicConfig),
761 #[serde(rename = "gemini")]
763 Gemini(gemini::GeminiConfig),
764}
765
766impl ProviderConfig {
767 pub fn id(&self) -> Option<ProviderId> {
771 ProviderId::new(self.vendor(), self.format())
772 }
773
774 pub fn vendor(&self) -> &'static str {
776 match self {
777 Self::OpenAi(provider) => provider.dialect.name,
778 Self::Anthropic(provider) => provider.dialect.name,
779 Self::Gemini(_) => gemini::PROVIDER_NAME,
780 }
781 }
782
783 pub fn format(&self) -> Format {
785 match self {
786 Self::OpenAi(_) => Format::OpenAi,
787 Self::Anthropic(_) => Format::Anthropic,
788 Self::Gemini(_) => Format::Gemini,
789 }
790 }
791
792 pub fn is_unauthenticated(&self) -> bool {
795 match self {
796 Self::OpenAi(provider) => provider.api_key.is_empty(),
797 Self::Anthropic(provider) => provider.api_key.is_empty(),
798 Self::Gemini(provider) => provider.api_key.is_empty(),
799 }
800 }
801
802 pub fn with_credential(mut self, api_key: impl Into<Secret>) -> Self {
805 let api_key = api_key.into();
806 match &mut self {
807 Self::OpenAi(provider) => provider.api_key = api_key,
808 Self::Anthropic(provider) => provider.api_key = api_key,
809 Self::Gemini(provider) => provider.api_key = api_key,
810 }
811 self
812 }
813
814 pub fn base_url(&self) -> &str {
823 match self {
824 Self::OpenAi(provider) => &provider.base_url,
825 Self::Anthropic(provider) => &provider.base_url,
826 Self::Gemini(provider) => &provider.base_url,
827 }
828 }
829
830 pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
834 let base_url = base_url.into();
835 match self {
836 Self::OpenAi(provider) => Self::OpenAi(provider.with_base_url(base_url)),
837 Self::Anthropic(provider) => Self::Anthropic(provider.with_base_url(base_url)),
838 Self::Gemini(provider) => Self::Gemini(provider.with_base_url(base_url)),
839 }
840 }
841
842 pub fn completion_handler(
846 &self,
847 label: &str,
848 model: &str,
849 http: DynHttpClient,
850 facts: ModelFacts,
851 ) -> ErasedHandler {
852 ErasedHandler::new(ModelAdapter::new(
853 label,
854 self.completion_model(model, http, facts),
855 ))
856 }
857
858 pub fn completion_model(
869 &self,
870 model: &str,
871 http: DynHttpClient,
872 facts: ModelFacts,
873 ) -> DynModel<Completion> {
874 match self {
875 Self::OpenAi(provider) => {
876 let wire = openai::wire::OpenAiWire::routed(provider.clone(), model, &facts)
877 .with_facts(facts);
878 provider.clone().connect(http).model(wire).erase()
879 }
880 Self::Anthropic(provider) => {
881 let mut model = provider.clone().connect(http).completion(model);
882 model.wire = model.wire.with_facts(facts);
883 model.erase()
884 }
885 Self::Gemini(provider) => {
886 let mut model = provider.clone().connect(http).completion(model);
887 model.wire = model.wire.with_facts(facts);
888 model.erase()
889 }
890 }
891 }
892}
893
894#[non_exhaustive]
897#[derive(Clone, Copy, Debug)]
898pub enum ModelSelector<'a> {
899 Spec(&'a ModelSpec),
901 Reference(&'a str),
903}
904
905impl<'a> From<&'a ModelSpec> for ModelSelector<'a> {
906 fn from(spec: &'a ModelSpec) -> Self {
907 Self::Spec(spec)
908 }
909}
910
911impl<'a> From<&'a str> for ModelSelector<'a> {
912 fn from(reference: &'a str) -> Self {
913 Self::Reference(reference)
914 }
915}
916
917impl<'a> From<&'a String> for ModelSelector<'a> {
918 fn from(reference: &'a String) -> Self {
919 Self::Reference(reference)
920 }
921}
922
923#[non_exhaustive]
938#[derive(Clone, Default)]
939pub struct ConnectOptions {
940 api_key: Option<Secret>,
941 account_id: Option<String>,
942 pub(crate) http: Option<DynHttpClient>,
943 format: Option<Format>,
944 base_url: Option<String>,
945}
946
947impl ConnectOptions {
948 pub fn new() -> Self {
950 Self::default()
951 }
952
953 pub fn api_key(mut self, api_key: impl Into<Secret>) -> Self {
956 self.api_key = Some(api_key.into());
957 self
958 }
959
960 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
973 self.account_id = Some(account_id.into());
974 self
975 }
976
977 pub fn http(mut self, http: impl HttpClientExt + 'static) -> Self {
979 self.http = Some(DynHttpClient::new(http));
980 self
981 }
982
983 pub fn format(mut self, format: Format) -> Self {
986 self.format = Some(format);
987 self
988 }
989
990 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
992 self.base_url = Some(base_url.into());
993 self
994 }
995}
996
997impl fmt::Debug for ConnectOptions {
999 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000 f.debug_struct("ConnectOptions")
1001 .field("api_key", &self.api_key.as_ref().map(|_| "<set>"))
1002 .field("account_id", &self.account_id.as_ref().map(|_| "<set>"))
1003 .field("http", &self.http.as_ref().map(|_| "<set>"))
1004 .field("format", &self.format)
1005 .field("base_url", &self.base_url)
1006 .finish()
1007 }
1008}
1009
1010#[non_exhaustive]
1012#[derive(Debug, thiserror::Error)]
1013pub enum ConnectError {
1014 #[error("`{reference}` names no model: expected `vendor/model`")]
1016 Malformed {
1017 reference: String,
1019 },
1020 #[error("the registry cannot connect to `{vendor}`: its models are served by {served_by}")]
1023 CatalogOnly {
1024 vendor: String,
1026 served_by: &'static str,
1028 },
1029 #[error(transparent)]
1032 NotFound(#[from] NotFound),
1033 #[error(transparent)]
1035 Selection(#[from] SelectionError),
1036 #[error(
1039 "no key for `{vendor}`: set {} or pass one with `ConnectOptions::api_key`",
1040 tried.iter().map(|name| format!("`{name}`")).collect::<Vec<_>>().join(" or ")
1041 )]
1042 MissingKey {
1043 vendor: String,
1045 tried: Vec<Cow<'static, str>>,
1048 },
1049 #[error("{0}")]
1052 Provider(String),
1053 #[error(transparent)]
1055 Env(#[from] EnvError),
1056 #[error("no HTTP client: pass one with `ConnectOptions::http`")]
1059 NoHttpClient,
1060}
1061
1062impl Catalog {
1063 #[cfg(feature = "reqwest")]
1077 #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
1078 pub fn connect<'a>(
1079 &self,
1080 model: impl Into<ModelSelector<'a>>,
1081 ) -> Result<DynModel<Completion>, ConnectError> {
1082 self.connect_with(model, ConnectOptions::new())
1083 }
1084
1085 pub fn connect_with<'a>(
1113 &self,
1114 model: impl Into<ModelSelector<'a>>,
1115 options: ConnectOptions,
1116 ) -> Result<DynModel<Completion>, ConnectError> {
1117 let (provider, model, facts) = match model.into() {
1118 ModelSelector::Spec(spec) => (
1119 spec.provider,
1120 spec.id.as_str(),
1121 ModelFacts::new(spec.clone()).with_catalog(self),
1122 ),
1123 ModelSelector::Reference(text) => {
1124 let reference = Reference::parse(text).ok_or_else(|| ConnectError::Malformed {
1125 reference: text.to_owned(),
1126 })?;
1127 let vendor =
1128 ProviderId::catalog(reference.vendor).ok_or_else(|| self.not_found(text))?;
1129 let provider = self
1130 .get_vendor(reference.vendor, reference.model)
1131 .map_or(vendor, |resolved| resolved.spec.provider);
1132 let provider = match reference.format {
1133 Some(format) if provider.is_registered() => {
1134 ProviderId::selection(reference.vendor, format)?
1135 }
1136 _ => provider,
1137 };
1138 (
1139 provider,
1140 reference.model,
1141 self.facts(provider, reference.model),
1142 )
1143 }
1144 };
1145 let provider = match options.format {
1146 Some(format) if provider.is_registered() => {
1147 ProviderId::selection(provider.vendor(), format)?
1148 }
1149 _ => provider,
1150 };
1151 let registered = provider.registered()?;
1152 let config = match options.api_key {
1153 Some(api_key) => registered.config(api_key),
1154 None => registered.config_from_env()?,
1155 };
1156 let config = match (config, options.account_id) {
1157 (ProviderConfig::OpenAi(provider), Some(account_id)) => {
1158 ProviderConfig::OpenAi(provider.with_account_id(account_id))
1159 }
1160 (config, _) => config,
1161 };
1162 let config = match options.base_url {
1163 Some(base_url) => config.with_base_url(base_url),
1164 None => config,
1165 };
1166 let http = match options.http {
1167 Some(http) => http,
1168 None => shared_http()?,
1169 };
1170 Ok(config.completion_model(model, http, facts))
1171 }
1172}
1173
1174#[cfg(feature = "reqwest")]
1176fn shared_http() -> Result<DynHttpClient, ConnectError> {
1177 Ok(rig_reqwest::shared())
1178}
1179
1180#[cfg(not(feature = "reqwest"))]
1182fn shared_http() -> Result<DynHttpClient, ConnectError> {
1183 Err(ConnectError::NoHttpClient)
1184}
1185
1186#[cfg(test)]
1187mod tests;