Skip to main content

cratefield_core/ports/
embedder.rs

1//! The `Embedder` port (issue #561, ADR 0024): text into embedding
2//! vectors, asked for by shape, never by vendor. A module holds
3//! `Arc<dyn Embedder>` and never learns which provider turned its text
4//! into numbers; the vectors feed the [`VectorIndex`](crate::VectorIndex)
5//! port, the sibling of this one — which is why [`Embeddings`] reports the
6//! answering model: vectors from different models are not comparable. No
7//! outcome enum, as on [`TextModel`](crate::TextModel): the unwired answer
8//! is [`EmbedError::NotConfigured`], so a module that cannot degrade
9//! without its embedder fails loudly.
10
11use async_trait::async_trait;
12
13/// One answered embedding request: one vector per input text, **in input
14/// order**, plus the model that answered and the input tokens it billed.
15#[derive(Debug, Clone, PartialEq)]
16pub struct Embeddings {
17    pub vectors: Vec<Vec<f32>>,
18    /// A provider identifier for the log line and the bill, not a wire
19    /// value a module switches on.
20    pub model: String,
21    pub input_tokens: u64,
22}
23
24impl Embeddings {
25    /// An answer by `model`; vectors and usage follow with
26    /// [`Embeddings::vector`] / [`Embeddings::usage`].
27    #[must_use]
28    pub fn new(model: impl Into<String>) -> Self {
29        Self {
30            vectors: Vec::new(),
31            model: model.into(),
32            input_tokens: 0,
33        }
34    }
35
36    /// Appends one vector, in input order.
37    #[must_use]
38    pub fn vector(mut self, vector: Vec<f32>) -> Self {
39        self.vectors.push(vector);
40        self
41    }
42
43    /// The token count the provider billed for the inputs.
44    #[must_use]
45    pub fn usage(mut self, input_tokens: u64) -> Self {
46        self.input_tokens = input_tokens;
47        self
48    }
49}
50
51/// Embedding failures. [`EmbedError::Provider`] carries the provider's own
52/// words, so it is scrubbed in `Display` (issue #235); `Debug` stays raw.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum EmbedError {
55    /// No embedder is wired — the venture did not provide one.
56    NotConfigured,
57    /// The request was malformed before any provider saw it.
58    InvalidInput(String),
59    /// The provider refused the request or the call failed.
60    Provider(String),
61}
62
63impl std::fmt::Display for EmbedError {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        let scrub = crate::logging::scrub_text;
66        match self {
67            Self::NotConfigured => f.write_str("no embedder is wired"),
68            Self::InvalidInput(message) => {
69                write!(f, "embed request rejected: {}", scrub(message))
70            }
71            Self::Provider(message) => write!(f, "embed failed: {}", scrub(message)),
72        }
73    }
74}
75
76impl std::error::Error for EmbedError {}
77
78/// Embeds texts, over whichever provider the venture wired: exactly one
79/// vector per input text, in input order, so a caller can zip them.
80#[async_trait]
81pub trait Embedder: Send + Sync {
82    /// Embeds every text in `texts`.
83    ///
84    /// # Errors
85    /// [`EmbedError::NotConfigured`] when no embedder is wired;
86    /// [`EmbedError::InvalidInput`] when the request is malformed;
87    /// [`EmbedError::Provider`] when the provider refused or failed.
88    async fn embed(&self, texts: &[String]) -> Result<Embeddings, EmbedError>;
89}