use std::fmt;
use std::hash::{Hash, Hasher};
use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::catalog::ModelSpec;
#[cfg(feature = "reqwest")]
use crate::client::env::{self, EnvError};
use crate::completion::ModelRef;
use crate::driver::DynModel;
use crate::http_client::{DynHttpClient, HttpClientExt};
use crate::operation::Completion;
use crate::providers::{anthropic, gemini, openai};
use crate::serve::ErasedHandler;
use crate::serve::adapters::ModelAdapter;
use crate::wire::Secret;
pub(crate) const OPENAI_DIALECTS: &[&openai::wire::Dialect] = &[
&openai::wire::OPENAI,
&openai::wire::AZURE,
&openai::wire::DEEPSEEK,
&openai::wire::GROQ,
&openai::wire::HYPERBOLIC,
&openai::wire::MIRA,
&openai::wire::PERPLEXITY,
&openai::wire::TOGETHER,
&openai::wire::HUGGINGFACE,
&openai::wire::LLAMACPP,
&openai::wire::MISTRAL,
&openai::wire::OPENROUTER,
&openai::wire::VENICE,
&openai::wire::DOUBLEWORD,
&openai::wire::ZAI,
&openai::wire::MINIMAX,
&openai::wire::MOONSHOT,
&openai::wire::XIAOMIMIMO,
&openai::wire::COHERE,
&openai::wire::OLLAMA,
&crate::providers::xai::DIALECT,
&crate::providers::chatgpt::DIALECT,
&crate::providers::copilot::wire::DIALECT,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Format {
OpenAi,
Anthropic,
Gemini,
}
impl Format {
pub const ALL: [Format; 3] = [Format::OpenAi, Format::Anthropic, Format::Gemini];
pub fn as_str(self) -> &'static str {
match self {
Format::OpenAi => "openai",
Format::Anthropic => "anthropic",
Format::Gemini => "gemini",
}
}
pub fn named(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|format| format.as_str() == name)
}
fn names() -> String {
Self::ALL
.iter()
.map(|format| format.as_str())
.collect::<Vec<_>>()
.join(", ")
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for Format {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Format {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let name = String::deserialize(deserializer)?;
Self::named(&name).ok_or_else(|| {
de::Error::custom(format!(
"`{name}` is not a protocol family ({})",
Format::names()
))
})
}
}
#[derive(Debug, Clone, Copy)]
enum Registered {
OpenAi(openai::wire::Dialect),
Anthropic(anthropic::wire::Dialect),
Gemini,
}
impl PartialEq for Registered {
fn eq(&self, other: &Self) -> bool {
self.vendor() == other.vendor() && self.format() == other.format()
}
}
impl Registered {
fn vendor(&self) -> &'static str {
match self {
Self::OpenAi(dialect) => dialect.name,
Self::Anthropic(dialect) => dialect.name,
Self::Gemini => gemini::PROVIDER_NAME,
}
}
fn format(&self) -> Format {
match self {
Self::OpenAi(_) => Format::OpenAi,
Self::Anthropic(_) => Format::Anthropic,
Self::Gemini => Format::Gemini,
}
}
fn config(&self, api_key: impl Into<Secret>) -> ProviderConfig {
match self {
Self::OpenAi(dialect) => {
ProviderConfig::OpenAi(openai::wire::OpenAIConfig::with_key(dialect, api_key))
}
Self::Anthropic(dialect) => ProviderConfig::Anthropic(
anthropic::wire::AnthropicConfig::with_key(dialect, api_key),
),
Self::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::new(api_key)),
}
}
#[cfg(feature = "reqwest")]
fn config_from_env(&self) -> Result<ProviderConfig, EnvError> {
Ok(match self {
Self::OpenAi(dialect) => {
let (api_key, auth) = openai_credential_from_env(dialect)?;
ProviderConfig::OpenAi(openai::wire::OpenAIConfig::from_env_with_credential(
dialect, api_key, auth,
)?)
}
Self::Anthropic(dialect) => {
ProviderConfig::Anthropic(anthropic::wire::AnthropicConfig::from_env_with(dialect)?)
}
Self::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::from_env()?),
})
}
}
#[derive(Debug)]
struct CatalogOnly {
vendor: &'static str,
home: &'static str,
credential: bool,
}
static CATALOG_ONLY: [CatalogOnly; 5] = [
CatalogOnly {
vendor: "aws_bedrock",
home: "the `rig-bedrock` crate",
credential: true,
},
CatalogOnly {
vendor: "vertexai",
home: "the `rig-vertexai` crate",
credential: true,
},
CatalogOnly {
vendor: "gemini-grpc",
home: "the `rig-gemini-grpc` crate",
credential: true,
},
CatalogOnly {
vendor: "candle",
home: "the `rig-candle` crate",
credential: false,
},
CatalogOnly {
vendor: "voyageai",
home: "`rig_core::providers::voyageai`, which serves embeddings and reranking only",
credential: true,
},
];
#[derive(Debug, Clone, Copy)]
enum Kind {
Registered(Registered),
CatalogOnly(&'static CatalogOnly),
}
#[derive(Debug, Clone, Copy)]
pub struct ProviderId(Kind);
impl PartialEq for ProviderId {
fn eq(&self, other: &Self) -> bool {
self.vendor() == other.vendor() && self.format() == other.format()
}
}
impl Eq for ProviderId {}
impl Hash for ProviderId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.vendor().hash(state);
self.format().hash(state);
}
}
impl ProviderId {
pub fn all() -> impl Iterator<Item = ProviderId> {
openai::wire::all()
.map(|dialect| Registered::OpenAi(*dialect))
.chain(anthropic::wire::all().map(|dialect| Registered::Anthropic(*dialect)))
.chain(std::iter::once(Registered::Gemini))
.map(|registered| ProviderId(Kind::Registered(registered)))
}
pub fn new(vendor: &str, format: Format) -> Option<Self> {
let registered = match format {
Format::OpenAi => {
openai::wire::by_name(vendor).map(|dialect| Registered::OpenAi(*dialect))
}
Format::Anthropic => {
anthropic::wire::Dialect::by_name(vendor).map(Registered::Anthropic)
}
Format::Gemini => (vendor == gemini::PROVIDER_NAME).then_some(Registered::Gemini),
};
registered.map(|registered| Self(Kind::Registered(registered)))
}
pub fn catalog(vendor: &str) -> Option<Self> {
Self::vendor_selections(vendor).next().or_else(|| {
CATALOG_ONLY
.iter()
.find(|provider| provider.vendor == vendor)
.map(|provider| Self(Kind::CatalogOnly(provider)))
})
}
pub fn vendor(&self) -> &'static str {
match &self.0 {
Kind::Registered(registered) => registered.vendor(),
Kind::CatalogOnly(provider) => provider.vendor,
}
}
pub fn format(&self) -> Option<Format> {
match &self.0 {
Kind::Registered(registered) => Some(registered.format()),
Kind::CatalogOnly(_) => None,
}
}
pub fn is_registered(&self) -> bool {
matches!(self.0, Kind::Registered(_))
}
pub(crate) fn served_by(&self) -> Option<&'static str> {
match &self.0 {
Kind::Registered(_) => None,
Kind::CatalogOnly(provider) => Some(provider.home),
}
}
pub fn vendor_selections(vendor: &str) -> impl Iterator<Item = ProviderId> + '_ {
Self::all().filter(move |id| id.vendor() == vendor)
}
pub fn resolve(selection: &str) -> Result<Self, SelectionError> {
let malformed = || SelectionError::Malformed {
selection: selection.to_owned(),
};
let (vendor, format) = match selection.split_once('/') {
Some((_, rest)) if rest.contains('/') => return Err(malformed()),
Some((vendor, format)) => (vendor, Some(format)),
None => (selection, None),
};
if vendor.is_empty() {
return Err(malformed());
}
let Some(format) = format else {
let mut registered = Self::vendor_selections(vendor);
let first = registered.next().ok_or_else(|| SelectionError::Unknown {
vendor: vendor.to_owned(),
})?;
return match registered.next() {
None => Ok(first),
Some(_) => Err(SelectionError::Ambiguous {
vendor: vendor.to_owned(),
alternatives: alternatives(vendor),
}),
};
};
let Some(family) = Format::named(format) else {
return Err(SelectionError::UnknownFormat {
format: format.to_owned(),
});
};
Self::new(vendor, family).ok_or_else(|| {
let alternatives = alternatives(vendor);
match alternatives.is_empty() {
true => SelectionError::Unknown {
vendor: vendor.to_owned(),
},
false => SelectionError::Unregistered {
vendor: vendor.to_owned(),
format: family,
alternatives,
},
}
})
}
pub fn config(&self, api_key: impl Into<Secret>) -> Option<ProviderConfig> {
match &self.0 {
Kind::Registered(registered) => Some(registered.config(api_key)),
Kind::CatalogOnly(_) => None,
}
}
pub fn api_key_env(&self) -> Option<&'static str> {
match &self.0 {
Kind::Registered(Registered::OpenAi(dialect)) => Some(dialect.api_key_env),
Kind::Registered(Registered::Anthropic(dialect)) => Some(dialect.api_key_env),
Kind::Registered(Registered::Gemini) => Some(gemini::API_KEY_ENV),
Kind::CatalogOnly(_) => None,
}
}
pub fn requires_credential(&self) -> bool {
match &self.0 {
Kind::Registered(Registered::OpenAi(dialect)) => {
!matches!(dialect.quirks.auth, openai::wire::Auth::OptionalBearer)
}
Kind::Registered(Registered::Anthropic(_) | Registered::Gemini) => true,
Kind::CatalogOnly(provider) => provider.credential,
}
}
}
fn alternatives(vendor: &str) -> Vec<String> {
ProviderId::vendor_selections(vendor)
.map(|id| id.to_string())
.collect()
}
impl fmt::Display for ProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.format() {
Some(format) => write!(f, "{}/{format}", self.vendor()),
None => f.write_str(self.vendor()),
}
}
}
impl Serialize for ProviderId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ProviderId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let selection = String::deserialize(deserializer)?;
Self::resolve(&selection)
.or_else(|error| {
Self::catalog(&selection)
.filter(|id| !id.is_registered())
.ok_or(error)
})
.map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SelectionError {
#[error("no registered provider is named `{vendor}`")]
Unknown {
vendor: String,
},
#[error(
"`{vendor}` speaks no {format} endpoint in this build (it speaks {})",
alternatives.join(", ")
)]
Unregistered {
vendor: String,
format: Format,
alternatives: Vec<String>,
},
#[error(
"`{vendor}` names more than one registered selection: name one of {}",
alternatives.join(", ")
)]
Ambiguous {
vendor: String,
alternatives: Vec<String>,
},
#[error("`{format}` is not a protocol family ({})", Format::names())]
UnknownFormat {
format: String,
},
#[error("`{selection}` is not a provider selection: expected `vendor` or `vendor/format`")]
Malformed {
selection: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ProviderConfig {
#[serde(rename = "openai")]
OpenAi(openai::wire::OpenAIConfig),
#[serde(rename = "anthropic")]
Anthropic(anthropic::wire::AnthropicConfig),
#[serde(rename = "gemini")]
Gemini(gemini::GeminiConfig),
}
impl ProviderConfig {
pub fn id(&self) -> Option<ProviderId> {
ProviderId::new(self.vendor(), self.format())
}
pub fn vendor(&self) -> &'static str {
match self {
Self::OpenAi(provider) => provider.dialect.name,
Self::Anthropic(provider) => provider.dialect.name,
Self::Gemini(_) => gemini::PROVIDER_NAME,
}
}
pub fn format(&self) -> Format {
match self {
Self::OpenAi(_) => Format::OpenAi,
Self::Anthropic(_) => Format::Anthropic,
Self::Gemini(_) => Format::Gemini,
}
}
pub fn is_unauthenticated(&self) -> bool {
match self {
Self::OpenAi(provider) => provider.api_key.is_empty(),
Self::Anthropic(provider) => provider.api_key.is_empty(),
Self::Gemini(provider) => provider.api_key.is_empty(),
}
}
pub fn with_credential(mut self, api_key: impl Into<Secret>) -> Self {
let api_key = api_key.into();
match &mut self {
Self::OpenAi(provider) => provider.api_key = api_key,
Self::Anthropic(provider) => provider.api_key = api_key,
Self::Gemini(provider) => provider.api_key = api_key,
}
self
}
pub fn base_url(&self) -> &str {
match self {
Self::OpenAi(provider) => &provider.base_url,
Self::Anthropic(provider) => &provider.base_url,
Self::Gemini(provider) => &provider.base_url,
}
}
pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
let base_url = base_url.into();
match self {
Self::OpenAi(provider) => Self::OpenAi(provider.with_base_url(base_url)),
Self::Anthropic(provider) => Self::Anthropic(provider.with_base_url(base_url)),
Self::Gemini(provider) => Self::Gemini(provider.with_base_url(base_url)),
}
}
pub fn completion_handler(
&self,
label: &str,
model: &str,
http: DynHttpClient,
) -> ErasedHandler {
ErasedHandler::new(ModelAdapter::new(label, self.completion_model(model, http)))
}
fn completion_model(&self, model: &str, http: DynHttpClient) -> DynModel<Completion> {
match self {
Self::OpenAi(provider) => provider.clone().connect(http).completion(model).erase(),
Self::Anthropic(provider) => provider.clone().connect(http).completion(model).erase(),
Self::Gemini(provider) => provider.clone().connect(http).completion(model).erase(),
}
}
#[cfg(feature = "reqwest")]
fn with_credential_from_env(self) -> Result<Self, EnvError> {
Ok(match self {
Self::OpenAi(mut provider) => {
let (api_key, auth) = openai_credential_from_env(&provider.dialect)?;
provider.api_key = api_key.into();
if auth != provider.dialect.quirks.auth {
provider.auth = auth;
}
Self::OpenAi(provider)
}
Self::Anthropic(mut provider) => {
provider.api_key = env::required(provider.dialect.api_key_env)?.into();
Self::Anthropic(provider)
}
Self::Gemini(mut provider) => {
provider.api_key = env::required(gemini::API_KEY_ENV)?.into();
Self::Gemini(provider)
}
})
}
}
#[cfg(feature = "reqwest")]
fn openai_credential_from_env(
dialect: &openai::wire::Dialect,
) -> Result<(String, openai::wire::Auth), EnvError> {
if matches!(dialect.quirks.auth, openai::wire::Auth::OptionalBearer)
&& dialect.alternate_auth.is_none()
{
let api_key = env::optional(dialect.api_key_env)?.unwrap_or_default();
return Ok((api_key, dialect.quirks.auth));
}
openai::wire::OpenAIConfig::credential_from_env(dialect)
}
#[derive(Clone, Debug, PartialEq)]
pub enum Provider {
Registered(ProviderId),
Configured(ProviderConfig),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProviderRef {
recipe: Recipe,
model: String,
}
#[derive(Clone, Debug, PartialEq)]
enum Recipe {
Registered(Registered),
Configured(ProviderConfig),
}
impl ProviderRef {
pub fn registered(id: ProviderId, model: impl Into<String>) -> Result<Self, RefError> {
match id.0 {
Kind::Registered(registered) => Self::new(Recipe::Registered(registered), model.into()),
Kind::CatalogOnly(provider) => Err(RefError::Selection(SelectionError::Unknown {
vendor: provider.vendor.to_owned(),
})),
}
}
pub fn configured(config: ProviderConfig, model: impl Into<String>) -> Result<Self, RefError> {
Self::new(Recipe::Configured(config.with_credential("")), model.into())
}
pub fn provider(&self) -> Provider {
match &self.recipe {
Recipe::Registered(registered) => {
Provider::Registered(ProviderId(Kind::Registered(*registered)))
}
Recipe::Configured(config) => Provider::Configured(config.clone()),
}
}
fn new(recipe: Recipe, model: String) -> Result<Self, RefError> {
if model.is_empty() {
return Err(RefError::EmptyModel);
}
Ok(Self { recipe, model })
}
pub fn model(&self) -> &str {
&self.model
}
pub fn parse(text: &str) -> Result<Self, RefError> {
let Some((selection, model)) = text.split_once(':') else {
return Err(RefError::NoModel {
reference: text.to_owned(),
});
};
if model.is_empty() {
return Err(RefError::NoModel {
reference: text.to_owned(),
});
}
Self::registered(ProviderId::resolve(selection)?, model)
}
pub fn id(&self) -> Option<ProviderId> {
match &self.recipe {
Recipe::Registered(registered) => Some(ProviderId(Kind::Registered(*registered))),
Recipe::Configured(config) => config.id(),
}
}
pub fn config(&self, api_key: impl Into<Secret>) -> ProviderConfig {
match &self.recipe {
Recipe::Registered(registered) => registered.config(api_key),
Recipe::Configured(config) => config.clone().with_credential(api_key),
}
}
}
impl ProviderRef {
#[cfg(feature = "reqwest")]
#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
pub fn completion_model(&self) -> Result<DynModel<Completion>, EnvError> {
let config = match &self.recipe {
Recipe::Registered(registered) => registered.config_from_env()?,
Recipe::Configured(config) => config.clone().with_credential_from_env()?,
};
Ok(config.completion_model(&self.model, rig_reqwest::shared()))
}
pub fn completion_model_with(
&self,
api_key: impl Into<Secret>,
http: impl HttpClientExt + 'static,
) -> DynModel<Completion> {
self.config(api_key)
.completion_model(&self.model, DynHttpClient::new(http))
}
}
impl std::str::FromStr for ProviderRef {
type Err = RefError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::parse(text)
}
}
impl fmt::Display for ProviderRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.recipe {
Recipe::Registered(registered) => {
write!(
f,
"{}/{}:{}",
registered.vendor(),
registered.format(),
self.model
)
}
Recipe::Configured(config) => {
write!(f, "{}/{}:{}", config.vendor(), config.format(), self.model)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RefError {
#[error("model identifier must not be empty")]
EmptyModel,
#[error("`{reference}` names no model: expected `vendor[/format]:model`")]
NoModel {
reference: String,
},
#[error(transparent)]
Selection(#[from] SelectionError),
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum ModelSelector<'a> {
Spec(&'a ModelSpec),
Reference(&'a str),
}
impl<'a> From<&'a ModelSpec> for ModelSelector<'a> {
fn from(spec: &'a ModelSpec) -> Self {
Self::Spec(spec)
}
}
impl<'a> From<&'a str> for ModelSelector<'a> {
fn from(reference: &'a str) -> Self {
Self::Reference(reference)
}
}
impl<'a> From<&'a String> for ModelSelector<'a> {
fn from(reference: &'a String) -> Self {
Self::Reference(reference)
}
}
impl<'a> From<&'a ModelRef> for ModelSelector<'a> {
fn from(reference: &'a ModelRef) -> Self {
Self::Reference(reference.as_str())
}
}
impl ModelSelector<'_> {
pub fn provider_ref(self) -> Result<ProviderRef, ConnectError> {
let (id, model) = match self {
Self::Spec(spec) => (spec.provider, spec.id.as_str()),
Self::Reference(reference) => {
let (vendor, model) =
crate::catalog::split_reference(reference).ok_or_else(|| {
ConnectError::Malformed {
reference: reference.to_owned(),
}
})?;
if let Some(id) = ProviderId::catalog(vendor).filter(|id| !id.is_registered()) {
(id, model)
} else if selection_grammar(reference) {
return Ok(ProviderRef::parse(reference)?);
} else {
let id = ProviderId::catalog(vendor).ok_or_else(|| {
RefError::Selection(SelectionError::Unknown {
vendor: vendor.to_owned(),
})
})?;
(id, model)
}
}
};
match id.served_by() {
Some(served_by) => Err(ConnectError::CatalogOnly {
vendor: id.vendor().to_owned(),
served_by,
}),
None => Ok(ProviderRef::registered(id, model)?),
}
}
}
fn selection_grammar(reference: &str) -> bool {
reference.split_once(':').is_some_and(|(selection, _)| {
selection
.split_once('/')
.is_none_or(|(_, format)| Format::named(format).is_some())
})
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConnectError {
#[error("`{reference}` names no model: expected `vendor/model` or `vendor[/format]:model`")]
Malformed {
reference: String,
},
#[error("the registry cannot connect to `{vendor}`: its models are served by {served_by}")]
CatalogOnly {
vendor: String,
served_by: &'static str,
},
#[error(transparent)]
Reference(#[from] RefError),
}
#[cfg(feature = "reqwest")]
#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
pub fn connect<'a>(
model: impl Into<ModelSelector<'a>>,
api_key: impl Into<Secret>,
) -> Result<DynModel<Completion>, ConnectError> {
let reference = model.into().provider_ref()?;
Ok(reference
.config(api_key)
.completion_model(reference.model(), rig_reqwest::shared()))
}
pub fn connect_with<'a>(
model: impl Into<ModelSelector<'a>>,
api_key: impl Into<Secret>,
http: impl HttpClientExt + 'static,
) -> Result<DynModel<Completion>, ConnectError> {
Ok(model
.into()
.provider_ref()?
.completion_model_with(api_key, http))
}
const REF_FIELDS: &[&str] = &["config", "model"];
impl Serialize for ProviderRef {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match &self.recipe {
Recipe::Registered(_) => serializer.collect_str(self),
Recipe::Configured(config) => {
let mut object = serializer.serialize_struct("ProviderRef", 2)?;
object.serialize_field("config", config)?;
object.serialize_field("model", &self.model)?;
object.end()
}
}
}
}
impl<'de> Deserialize<'de> for ProviderRef {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(RefVisitor)
}
}
struct RefVisitor;
impl<'de> Visitor<'de> for RefVisitor {
type Value = ProviderRef;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"a provider reference `vendor[/format]:model`, or an object with `config` and `model`",
)
}
fn visit_str<E: de::Error>(self, text: &str) -> Result<Self::Value, E> {
ProviderRef::parse(text).map_err(de::Error::custom)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut config: Option<ProviderConfig> = None;
let mut model: Option<String> = None;
while let Some(field) = map.next_key::<String>()? {
match field.as_str() {
"config" => {
if config.is_some() {
return Err(de::Error::duplicate_field("config"));
}
config = Some(map.next_value()?);
}
"model" => {
if model.is_some() {
return Err(de::Error::duplicate_field("model"));
}
model = Some(map.next_value()?);
}
unknown => return Err(de::Error::unknown_field(unknown, REF_FIELDS)),
}
}
ProviderRef::configured(
config.ok_or_else(|| de::Error::missing_field("config"))?,
model.ok_or_else(|| de::Error::missing_field("model"))?,
)
.map_err(de::Error::custom)
}
}
#[cfg(test)]
mod tests;