Skip to main content

rig_core/providers/
registry.rs

1//! Serializable provider selections, explicit configurations, and model references.
2//!
3//! [`ProviderRef`] pairs a nonempty model ID with a registered preset or an
4//! explicit configuration. References discard credentials and require a
5//! self-describing serialization format; hosts supply credentials at execution.
6//!
7//! ```
8//! use rig_core::providers::registry::ProviderRef;
9//! let reference = ProviderRef::parse("deepseek:deepseek-chat")?;
10//! assert_eq!(reference.to_string(), "deepseek/openai:deepseek-chat");
11//! # Ok::<(), rig_core::providers::registry::RefError>(())
12//! ```
13
14use std::fmt;
15use std::hash::{Hash, Hasher};
16
17use serde::de::{self, MapAccess, Visitor};
18use serde::ser::SerializeStruct;
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21#[cfg(feature = "reqwest")]
22use crate::client::env::{self, EnvError};
23use crate::driver::DynModel;
24use crate::http_client::{DynHttpClient, HttpClientExt};
25use crate::operation::Completion;
26use crate::providers::{anthropic, gemini, openai};
27use crate::serve::ErasedHandler;
28use crate::serve::adapters::ModelAdapter;
29use crate::wire::Secret;
30
31/// Every dialect this build knows, for
32/// [`openai::wire::Dialect`]'s [`Deserialize`](serde::Deserialize) lookup.
33///
34/// Keyed by [`openai::wire::Dialect::name`], so the regional and endpoint variants that
35/// share a provider name are not listed: a stored wire keeps its base URL,
36/// which is what distinguishes them.
37pub(crate) const OPENAI_DIALECTS: &[&openai::wire::Dialect] = &[
38    &openai::wire::OPENAI,
39    &openai::wire::AZURE,
40    &openai::wire::DEEPSEEK,
41    &openai::wire::GROQ,
42    &openai::wire::HYPERBOLIC,
43    &openai::wire::MIRA,
44    &openai::wire::PERPLEXITY,
45    &openai::wire::TOGETHER,
46    &openai::wire::HUGGINGFACE,
47    &openai::wire::LLAMACPP,
48    &openai::wire::MISTRAL,
49    &openai::wire::OPENROUTER,
50    &openai::wire::VENICE,
51    &openai::wire::DOUBLEWORD,
52    &openai::wire::ZAI,
53    &openai::wire::MINIMAX,
54    &openai::wire::MOONSHOT,
55    &openai::wire::XIAOMIMIMO,
56    &crate::providers::xai::DIALECT,
57    &crate::providers::chatgpt::DIALECT,
58    &crate::providers::copilot::wire::DIALECT,
59];
60
61/// A provider's request grammar and configuration type.
62/// The OpenAI family includes Chat Completions and Responses; select the endpoint
63/// through [`openai::OpenAIConfig::with_route`].
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub enum Format {
66    /// OpenAI's grammar: Chat Completions and the Responses endpoint.
67    OpenAi,
68    /// Anthropic's Messages grammar.
69    Anthropic,
70    /// Gemini's GenerateContent grammar.
71    Gemini,
72}
73
74impl Format {
75    /// Every protocol family, in registration order.
76    pub const ALL: [Format; 3] = [Format::OpenAi, Format::Anthropic, Format::Gemini];
77
78    /// The family's stable serialized name.
79    pub fn as_str(self) -> &'static str {
80        match self {
81            Format::OpenAi => "openai",
82            Format::Anthropic => "anthropic",
83            Format::Gemini => "gemini",
84        }
85    }
86
87    /// The family `name` spells, or `None`.
88    pub fn named(name: &str) -> Option<Self> {
89        Self::ALL.into_iter().find(|format| format.as_str() == name)
90    }
91
92    /// Every family's name, for an error message.
93    fn names() -> String {
94        Self::ALL
95            .iter()
96            .map(|format| format.as_str())
97            .collect::<Vec<_>>()
98            .join(", ")
99    }
100}
101
102impl fmt::Display for Format {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str(self.as_str())
105    }
106}
107
108impl Serialize for Format {
109    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110        serializer.serialize_str(self.as_str())
111    }
112}
113
114impl<'de> Deserialize<'de> for Format {
115    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116        let name = String::deserialize(deserializer)?;
117        Self::named(&name).ok_or_else(|| {
118            de::Error::custom(format!(
119                "`{name}` is not a protocol family ({})",
120                Format::names()
121            ))
122        })
123    }
124}
125
126/// Registered dialect definition and its configuration family.
127#[derive(Debug, Clone, Copy)]
128enum Registered {
129    OpenAi(openai::wire::Dialect),
130    Anthropic(anthropic::wire::Dialect),
131    Gemini,
132}
133
134/// Validated vendor and protocol-family pair from this build's dialect tables.
135/// Construction and deserialization reject unregistered pairs. Equality and
136/// hashing use the pair, not dialect options; display emits `vendor/format`.
137#[derive(Debug, Clone, Copy)]
138pub struct ProviderId(Registered);
139
140/// Identity is the `(vendor, format)` pair, never the dialect's payload: two
141/// ids that name the same selection are the same id.
142impl PartialEq for ProviderId {
143    fn eq(&self, other: &Self) -> bool {
144        self.vendor() == other.vendor() && self.format() == other.format()
145    }
146}
147
148impl Eq for ProviderId {}
149
150impl Hash for ProviderId {
151    fn hash<H: Hasher>(&self, state: &mut H) {
152        self.vendor().hash(state);
153        self.format().hash(state);
154    }
155}
156
157impl ProviderId {
158    /// Every selection this build registers: one per OpenAI-shaped dialect,
159    /// one per Messages-format dialect, and Gemini.
160    pub fn all() -> impl Iterator<Item = ProviderId> {
161        openai::wire::all()
162            .map(|dialect| ProviderId(Registered::OpenAi(*dialect)))
163            .chain(
164                anthropic::wire::all().map(|dialect| ProviderId(Registered::Anthropic(*dialect))),
165            )
166            .chain(std::iter::once(ProviderId(Registered::Gemini)))
167    }
168
169    /// The selection `vendor` names in `format`, or `None` when this build
170    /// registers no such pair.
171    pub fn new(vendor: &str, format: Format) -> Option<Self> {
172        match format {
173            Format::OpenAi => {
174                openai::wire::by_name(vendor).map(|dialect| Self(Registered::OpenAi(*dialect)))
175            }
176            Format::Anthropic => anthropic::wire::Dialect::by_name(vendor)
177                .map(|dialect| Self(Registered::Anthropic(dialect))),
178            Format::Gemini => (vendor == gemini::PROVIDER_NAME).then_some(Self(Registered::Gemini)),
179        }
180    }
181
182    /// The vendor, spelled as the provider's own descriptor name.
183    pub fn vendor(&self) -> &'static str {
184        match &self.0 {
185            Registered::OpenAi(dialect) => dialect.name,
186            Registered::Anthropic(dialect) => dialect.name,
187            Registered::Gemini => gemini::PROVIDER_NAME,
188        }
189    }
190
191    /// The protocol family.
192    pub fn format(&self) -> Format {
193        match &self.0 {
194            Registered::OpenAi(_) => Format::OpenAi,
195            Registered::Anthropic(_) => Format::Anthropic,
196            Registered::Gemini => Format::Gemini,
197        }
198    }
199
200    /// Every selection registered for `vendor`, in registration order.
201    pub fn vendor_selections(vendor: &str) -> impl Iterator<Item = ProviderId> + '_ {
202        Self::all().filter(move |id| id.vendor() == vendor)
203    }
204
205    /// Resolve `vendor` or `vendor/format`.
206    ///
207    /// A bare vendor is accepted only when this build registers exactly one
208    /// family for it; otherwise the error names the qualified alternatives,
209    /// each of which this resolver accepts.
210    pub fn resolve(selection: &str) -> Result<Self, SelectionError> {
211        let malformed = || SelectionError::Malformed {
212            selection: selection.to_owned(),
213        };
214        let (vendor, format) = match selection.split_once('/') {
215            Some((_, rest)) if rest.contains('/') => return Err(malformed()),
216            Some((vendor, format)) => (vendor, Some(format)),
217            None => (selection, None),
218        };
219        if vendor.is_empty() {
220            return Err(malformed());
221        }
222        let Some(format) = format else {
223            let mut registered = Self::vendor_selections(vendor);
224            let first = registered.next().ok_or_else(|| SelectionError::Unknown {
225                vendor: vendor.to_owned(),
226            })?;
227            return match registered.next() {
228                None => Ok(first),
229                Some(_) => Err(SelectionError::Ambiguous {
230                    vendor: vendor.to_owned(),
231                    alternatives: alternatives(vendor),
232                }),
233            };
234        };
235        let Some(family) = Format::named(format) else {
236            return Err(SelectionError::UnknownFormat {
237                format: format.to_owned(),
238            });
239        };
240        Self::new(vendor, family).ok_or_else(|| {
241            let alternatives = alternatives(vendor);
242            match alternatives.is_empty() {
243                true => SelectionError::Unknown {
244                    vendor: vendor.to_owned(),
245                },
246                false => SelectionError::Unregistered {
247                    vendor: vendor.to_owned(),
248                    format: family,
249                    alternatives,
250                },
251            }
252        })
253    }
254
255    /// Build this selection's preset with `api_key`. Copilot requires an exchanged
256    /// session token, not a GitHub OAuth token.
257    pub fn config(&self, api_key: impl Into<Secret>) -> ProviderConfig {
258        match &self.0 {
259            Registered::OpenAi(dialect) => {
260                ProviderConfig::OpenAi(openai::wire::OpenAIConfig::with_key(dialect, api_key))
261            }
262            Registered::Anthropic(dialect) => ProviderConfig::Anthropic(
263                anthropic::wire::AnthropicConfig::with_dialect(api_key, dialect),
264            ),
265            Registered::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::new(api_key)),
266        }
267    }
268
269    /// This selection's preset, configured from the environment variables
270    /// its dialect names.
271    #[cfg(feature = "reqwest")]
272    fn config_from_env(&self) -> Result<ProviderConfig, EnvError> {
273        Ok(match &self.0 {
274            Registered::OpenAi(dialect) => {
275                let (api_key, auth) = openai_credential_from_env(dialect)?;
276                ProviderConfig::OpenAi(openai::wire::OpenAIConfig::from_env_with_credential(
277                    dialect, api_key, auth,
278                )?)
279            }
280            Registered::Anthropic(dialect) => {
281                ProviderConfig::Anthropic(anthropic::wire::AnthropicConfig::from_env_with(dialect)?)
282            }
283            Registered::Gemini => ProviderConfig::Gemini(gemini::GeminiConfig::from_env()?),
284        })
285    }
286
287    /// Environment variable named by the registered credential configuration.
288    pub fn api_key_env(&self) -> &'static str {
289        match &self.0 {
290            Registered::OpenAi(dialect) => dialect.api_key_env,
291            Registered::Anthropic(dialect) => dialect.api_key_env,
292            Registered::Gemini => gemini::API_KEY_ENV,
293        }
294    }
295
296    /// Whether this selection needs a credential at all.
297    ///
298    /// A local `llama-server` authenticates optionally, so naming its
299    /// variable is a hint rather than a requirement.
300    pub fn requires_credential(&self) -> bool {
301        match &self.0 {
302            Registered::OpenAi(dialect) => {
303                !matches!(dialect.quirks.auth, openai::wire::Auth::OptionalBearer)
304            }
305            Registered::Anthropic(_) | Registered::Gemini => true,
306        }
307    }
308}
309
310/// The qualified spellings registered for `vendor`, in registration order.
311fn alternatives(vendor: &str) -> Vec<String> {
312    ProviderId::vendor_selections(vendor)
313        .map(|id| id.to_string())
314        .collect()
315}
316
317impl fmt::Display for ProviderId {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}/{}", self.vendor(), self.format())
320    }
321}
322
323impl Serialize for ProviderId {
324    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
325        serializer.collect_str(self)
326    }
327}
328
329impl<'de> Deserialize<'de> for ProviderId {
330    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
331        let selection = String::deserialize(deserializer)?;
332        Self::resolve(&selection).map_err(de::Error::custom)
333    }
334}
335
336/// Why a provider selection did not resolve.
337#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
338pub enum SelectionError {
339    /// No registered provider goes by this vendor name.
340    #[error("no registered provider is named `{vendor}`")]
341    Unknown {
342        /// The vendor named.
343        vendor: String,
344    },
345    /// The vendor is registered, but not for this protocol family.
346    #[error(
347        "`{vendor}` speaks no {format} endpoint in this build (it speaks {})",
348        alternatives.join(", ")
349    )]
350    Unregistered {
351        /// The vendor named.
352        vendor: String,
353        /// The family named.
354        format: Format,
355        /// The vendor's registered selections, canonically spelled.
356        alternatives: Vec<String>,
357    },
358    /// A bare vendor with more than one registered family.
359    #[error(
360        "`{vendor}` names more than one registered selection: name one of {}",
361        alternatives.join(", ")
362    )]
363    Ambiguous {
364        /// The vendor named.
365        vendor: String,
366        /// The vendor's registered selections, canonically spelled.
367        alternatives: Vec<String>,
368    },
369    /// The family is not one this crate knows.
370    #[error("`{format}` is not a protocol family ({})", Format::names())]
371    UnknownFormat {
372        /// The family named.
373        format: String,
374    },
375    /// Not `vendor` or `vendor/format` at all.
376    #[error("`{selection}` is not a provider selection: expected `vendor` or `vendor/format`")]
377    Malformed {
378        /// What was given.
379        selection: String,
380    },
381}
382
383/// A provider configuration tagged by protocol family, such as `{"openai": {…}}`.
384/// Serialization rejects unregistered or modified dialect definitions. Set
385/// persistent hosts and typed options on the configuration, not the dialect.
386/// Deserialization reports unknown configuration fields by name.
387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
388pub enum ProviderConfig {
389    /// An OpenAI-shaped provider, on either of its two endpoints.
390    #[serde(rename = "openai")]
391    OpenAi(openai::wire::OpenAIConfig),
392    /// A Messages-format provider.
393    #[serde(rename = "anthropic")]
394    Anthropic(anthropic::wire::AnthropicConfig),
395    /// Gemini.
396    #[serde(rename = "gemini")]
397    Gemini(gemini::GeminiConfig),
398}
399
400impl ProviderConfig {
401    /// The catalog selection with this dialect's name, if registered.
402    /// Host and option overrides do not change the selection; its preset
403    /// remains the catalog's, never a custom dialect's payload.
404    pub fn id(&self) -> Option<ProviderId> {
405        ProviderId::new(self.vendor(), self.format())
406    }
407
408    /// The configured dialect's name, including unregistered dialects.
409    pub fn vendor(&self) -> &'static str {
410        match self {
411            Self::OpenAi(provider) => provider.dialect.name,
412            Self::Anthropic(provider) => provider.dialect.name,
413            Self::Gemini(_) => gemini::PROVIDER_NAME,
414        }
415    }
416
417    /// The configuration family, independently of catalog membership.
418    pub fn format(&self) -> Format {
419        match self {
420            Self::OpenAi(_) => Format::OpenAi,
421            Self::Anthropic(_) => Format::Anthropic,
422            Self::Gemini(_) => Format::Gemini,
423        }
424    }
425
426    /// Whether the credential is empty, as it is after serialization round-trip
427    /// because [`Secret`] omits its value.
428    pub fn is_unauthenticated(&self) -> bool {
429        match self {
430            Self::OpenAi(provider) => provider.api_key.is_empty(),
431            Self::Anthropic(provider) => provider.api_key.is_empty(),
432            Self::Gemini(provider) => provider.api_key.is_empty(),
433        }
434    }
435
436    /// The same configuration with `api_key` as its credential: how a host
437    /// rehydrates a configuration it loaded from data.
438    pub fn with_credential(mut self, api_key: impl Into<Secret>) -> Self {
439        let api_key = api_key.into();
440        match &mut self {
441            Self::OpenAi(provider) => provider.api_key = api_key,
442            Self::Anthropic(provider) => provider.api_key = api_key,
443            Self::Gemini(provider) => provider.api_key = api_key,
444        }
445        self
446    }
447
448    /// The completion wire for `model`, bound to `http`, erased behind a
449    /// [`ModelAdapter`] labelled `label`.
450    ///
451    /// A handler built from data uses the provider's own completion wire,
452    /// including Copilot's model-dependent routing and request envelope.
453    /// Explicit hosts, routes and typed options are preserved.
454    pub fn completion_handler(
455        &self,
456        label: &str,
457        model: &str,
458        http: DynHttpClient,
459    ) -> ErasedHandler {
460        ErasedHandler::new(ModelAdapter::new(label, self.completion_model(model, http)))
461    }
462
463    /// The provider's completion model for `model` on `http`, erased.
464    fn completion_model(&self, model: &str, http: DynHttpClient) -> DynModel<Completion> {
465        match self {
466            Self::OpenAi(provider) => provider.clone().connect(http).completion(model).erase(),
467            Self::Anthropic(provider) => provider.clone().connect(http).completion(model).erase(),
468            Self::Gemini(provider) => provider.clone().connect(http).completion(model).erase(),
469        }
470    }
471
472    /// The credential this configuration's vendor reads from the
473    /// environment. A vendor whose credential is optional reads an unset
474    /// variable as no credential.
475    #[cfg(feature = "reqwest")]
476    fn with_credential_from_env(self) -> Result<Self, EnvError> {
477        Ok(match self {
478            Self::OpenAi(mut provider) => {
479                let (api_key, auth) = openai_credential_from_env(&provider.dialect)?;
480                provider.api_key = api_key.into();
481                // Only an alternative credential changes how it is sent.
482                if auth != provider.dialect.quirks.auth {
483                    provider.auth = auth;
484                }
485                Self::OpenAi(provider)
486            }
487            Self::Anthropic(mut provider) => {
488                provider.api_key = env::required(provider.dialect.api_key_env)?.into();
489                Self::Anthropic(provider)
490            }
491            Self::Gemini(mut provider) => {
492                provider.api_key = env::required(gemini::API_KEY_ENV)?.into();
493                Self::Gemini(provider)
494            }
495        })
496    }
497}
498
499/// The credential an OpenAI-family `dialect` reads from the environment:
500/// its alternative when only that is set, and none when the dialect
501/// authenticates optionally and nothing is set.
502#[cfg(feature = "reqwest")]
503fn openai_credential_from_env(
504    dialect: &openai::wire::Dialect,
505) -> Result<(String, openai::wire::Auth), EnvError> {
506    if matches!(dialect.quirks.auth, openai::wire::Auth::OptionalBearer)
507        && dialect.alternate_auth.is_none()
508    {
509        let api_key = env::optional(dialect.api_key_env)?.unwrap_or_default();
510        return Ok((api_key, dialect.quirks.auth));
511    }
512    openai::wire::OpenAIConfig::credential_from_env(dialect)
513}
514
515/// Which provider a [`ProviderRef`] names: the registry's preset for a
516/// selection, or an explicit configuration.
517#[derive(Clone, Debug, PartialEq)]
518pub enum Provider {
519    /// A registered selection, whose configuration is the registry's preset.
520    Registered(ProviderId),
521    /// An explicit configuration, carrying its own host and options.
522    Configured(ProviderConfig),
523}
524
525/// A nonempty model identifier paired with a credential-free provider selection.
526/// Construction validates the identifier; both fields are read-only.
527///
528/// ```compile_fail
529/// use rig_core::providers::registry::ProviderRef;
530/// let Ok(mut reference) = ProviderRef::parse("deepseek:deepseek-chat") else { return; };
531/// reference.model.clear(); // the validated identifier is private
532/// ```
533///
534/// Provider data is also read-only, so credentials cannot be inserted afterwards:
535///
536/// ```compile_fail
537/// use rig_core::providers::registry::ProviderRef;
538/// let Ok(mut reference) = ProviderRef::parse("deepseek:deepseek-chat") else { return; };
539/// reference.provider = reference.provider().clone();
540/// ```
541#[derive(Clone, Debug, PartialEq)]
542pub struct ProviderRef {
543    /// The provider.
544    provider: Provider,
545    /// The provider's own model identifier. Non-empty: the string form
546    /// separates the selection from the model at the first `:`, so an empty
547    /// model has no spelling [`parse`](Self::parse) would read back.
548    model: String,
549}
550
551impl ProviderRef {
552    /// A reference to a non-empty `model` on a registered selection.
553    /// Returns [`RefError::EmptyModel`] for an empty identifier.
554    pub fn registered(id: ProviderId, model: impl Into<String>) -> Result<Self, RefError> {
555        Self::new(Provider::Registered(id), model.into())
556    }
557
558    /// A reference to a non-empty `model` on an explicit configuration.
559    /// Removes the credential: references are persistent recipes, and the
560    /// host supplies credentials through [`config`](Self::config) at construction time.
561    /// The host stays fixed, even if it is a preset's default. For Copilot,
562    /// use a registered reference to follow the resolved token's proxy endpoint,
563    /// or configure the intended endpoint before creating this reference.
564    /// Returns [`RefError::EmptyModel`] for an empty identifier.
565    pub fn configured(config: ProviderConfig, model: impl Into<String>) -> Result<Self, RefError> {
566        Self::new(
567            Provider::Configured(config.with_credential("")),
568            model.into(),
569        )
570    }
571
572    /// The credential-free provider recipe.
573    pub fn provider(&self) -> &Provider {
574        &self.provider
575    }
576
577    fn new(provider: Provider, model: String) -> Result<Self, RefError> {
578        if model.is_empty() {
579            return Err(RefError::EmptyModel);
580        }
581        Ok(Self { provider, model })
582    }
583
584    /// The non-empty model identifier.
585    pub fn model(&self) -> &str {
586        &self.model
587    }
588
589    /// Parse `vendor[/format]:model`.
590    ///
591    /// The first `:` separates the selection from the model, so a model
592    /// identifier may contain `:` and `/` freely.
593    pub fn parse(text: &str) -> Result<Self, RefError> {
594        let Some((selection, model)) = text.split_once(':') else {
595            return Err(RefError::NoModel {
596                reference: text.to_owned(),
597            });
598        };
599        if model.is_empty() {
600            return Err(RefError::NoModel {
601                reference: text.to_owned(),
602            });
603        }
604        Self::registered(ProviderId::resolve(selection)?, model)
605    }
606
607    /// The catalog selection this reference names, if its dialect is registered.
608    pub fn id(&self) -> Option<ProviderId> {
609        match &self.provider {
610            Provider::Registered(id) => Some(*id),
611            Provider::Configured(config) => config.id(),
612        }
613    }
614
615    /// The configuration to build from, credentialed with `api_key`: the
616    /// registry's preset, or the explicit configuration rehydrated.
617    pub fn config(&self, api_key: impl Into<Secret>) -> ProviderConfig {
618        match &self.provider {
619            Provider::Registered(id) => id.config(api_key),
620            Provider::Configured(config) => config.clone().with_credential(api_key),
621        }
622    }
623}
624
625impl ProviderRef {
626    /// A completion model for this reference, credentials from the
627    /// environment, on the shared reqwest client. A registered selection
628    /// reads every variable its dialect names; an explicit configuration
629    /// keeps its host and options and reads only its vendor's credential.
630    #[cfg(feature = "reqwest")]
631    #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
632    pub fn completion_model(&self) -> Result<DynModel<Completion>, EnvError> {
633        let config = match &self.provider {
634            Provider::Registered(id) => id.config_from_env()?,
635            Provider::Configured(config) => config.clone().with_credential_from_env()?,
636        };
637        Ok(config.completion_model(&self.model, rig_reqwest::shared()))
638    }
639
640    /// A completion model for this reference, credentialed with `api_key`,
641    /// sending through `http`.
642    pub fn completion_model_with(
643        &self,
644        api_key: impl Into<Secret>,
645        http: impl HttpClientExt + 'static,
646    ) -> DynModel<Completion> {
647        self.config(api_key)
648            .completion_model(&self.model, DynHttpClient::new(http))
649    }
650}
651
652impl std::str::FromStr for ProviderRef {
653    type Err = RefError;
654
655    fn from_str(text: &str) -> Result<Self, Self::Err> {
656        Self::parse(text)
657    }
658}
659
660/// Display `vendor/format:model` for registered and configured references.
661/// This label omits hosts and options; use serialization to preserve them.
662impl fmt::Display for ProviderRef {
663    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
664        match &self.provider {
665            Provider::Registered(id) => write!(f, "{id}:{}", self.model),
666            Provider::Configured(config) => {
667                write!(f, "{}/{}:{}", config.vendor(), config.format(), self.model)
668            }
669        }
670    }
671}
672
673/// Why a provider reference did not parse.
674#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
675pub enum RefError {
676    /// A constructor or structured reference supplied an empty model identifier.
677    #[error("model identifier must not be empty")]
678    EmptyModel,
679    /// No model identifier after the selection.
680    #[error("`{reference}` names no model: expected `vendor[/format]:model`")]
681    NoModel {
682        /// What was given.
683        reference: String,
684    },
685    /// The selection did not resolve.
686    #[error(transparent)]
687    Selection(#[from] SelectionError),
688}
689
690/// The field names of the object form, which is also what a wrong shape is
691/// reported against.
692const REF_FIELDS: &[&str] = &["config", "model"];
693
694/// Serialize registered references as canonical strings and configured references
695/// as `{config, model}` objects to preserve their hosts and options.
696impl Serialize for ProviderRef {
697    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
698        match &self.provider {
699            Provider::Registered(_) => serializer.collect_str(self),
700            Provider::Configured(config) => {
701                let mut object = serializer.serialize_struct("ProviderRef", 2)?;
702                object.serialize_field("config", config)?;
703                object.serialize_field("model", &self.model)?;
704                object.end()
705            }
706        }
707    }
708}
709
710/// Deserialize strings as registered references and maps as explicit configurations.
711/// Requires a self-describing format and preserves configuration field errors.
712impl<'de> Deserialize<'de> for ProviderRef {
713    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
714        deserializer.deserialize_any(RefVisitor)
715    }
716}
717
718struct RefVisitor;
719
720impl<'de> Visitor<'de> for RefVisitor {
721    type Value = ProviderRef;
722
723    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        f.write_str(
725            "a provider reference `vendor[/format]:model`, or an object with `config` and `model`",
726        )
727    }
728
729    fn visit_str<E: de::Error>(self, text: &str) -> Result<Self::Value, E> {
730        ProviderRef::parse(text).map_err(de::Error::custom)
731    }
732
733    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
734        let mut config: Option<ProviderConfig> = None;
735        let mut model: Option<String> = None;
736        while let Some(field) = map.next_key::<String>()? {
737            match field.as_str() {
738                "config" => {
739                    if config.is_some() {
740                        return Err(de::Error::duplicate_field("config"));
741                    }
742                    config = Some(map.next_value()?);
743                }
744                "model" => {
745                    if model.is_some() {
746                        return Err(de::Error::duplicate_field("model"));
747                    }
748                    model = Some(map.next_value()?);
749                }
750                unknown => return Err(de::Error::unknown_field(unknown, REF_FIELDS)),
751            }
752        }
753        ProviderRef::configured(
754            config.ok_or_else(|| de::Error::missing_field("config"))?,
755            model.ok_or_else(|| de::Error::missing_field("model"))?,
756        )
757        .map_err(de::Error::custom)
758    }
759}
760
761#[cfg(test)]
762mod tests;