Skip to main content

ferro_ai/
embed.rs

1//! Text embedding entry point for the ferro-ai SDK.
2//!
3//! [`embed`] is the primary surface for generating text embeddings.
4//! It is a thin delegate over [`LlmClient::embed`], symmetric with
5//! [`crate::complete()`].
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use ferro_ai::{embed, OllamaClient};
11//!
12//! let client = OllamaClient::new(None, None);
13//! let vector: Vec<f32> = embed(&client, "Hello, world!").await?;
14//! ```
15//!
16//! ## Provider support
17//!
18//! - `OllamaClient` — posts to `/api/embed` using the model from
19//!   `FERRO_AI_EMBED_MODEL` (default `nomic-embed-text`).
20//! - `OpenAiClient` — posts to `/v1/embeddings` using the model from
21//!   `FERRO_AI_EMBED_MODEL` (default `text-embedding-3-small`).
22//! - `AnthropicClient` — returns `Err(Error::Unsupported)`; Anthropic has no
23//!   embeddings endpoint.
24
25use crate::client::LlmClient;
26use crate::error::Error;
27
28/// Generate a text embedding vector using the configured LLM provider.
29///
30/// Thin pass-through to [`LlmClient::embed`]: no batching, normalization, or retry.
31/// Symmetric with [`crate::complete()`].
32///
33/// Returns `Err(Error::Unsupported)` for providers without an embeddings
34/// endpoint (e.g. `AnthropicClient`).
35pub async fn embed(client: &dyn LlmClient, text: &str) -> Result<Vec<f32>, Error> {
36    client.embed(text).await
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use async_trait::async_trait;
43
44    use crate::client::{CompletionRequest, TokenStream};
45
46    struct OkClient;
47
48    #[async_trait]
49    impl LlmClient for OkClient {
50        fn default_model(&self) -> &str {
51            "test"
52        }
53
54        async fn complete(&self, _: CompletionRequest) -> Result<String, Error> {
55            Err(Error::Unsupported)
56        }
57
58        async fn complete_stream(&self, _: CompletionRequest) -> Result<TokenStream, Error> {
59            Err(Error::Unsupported)
60        }
61
62        async fn embed(&self, _: &str) -> Result<Vec<f32>, Error> {
63            Ok(vec![0.1, 0.2, 0.3])
64        }
65    }
66
67    struct UnsupportedClient;
68
69    #[async_trait]
70    impl LlmClient for UnsupportedClient {
71        fn default_model(&self) -> &str {
72            "test"
73        }
74
75        async fn complete(&self, _: CompletionRequest) -> Result<String, Error> {
76            Err(Error::Unsupported)
77        }
78
79        async fn complete_stream(&self, _: CompletionRequest) -> Result<TokenStream, Error> {
80            Err(Error::Unsupported)
81        }
82
83        async fn embed(&self, _: &str) -> Result<Vec<f32>, Error> {
84            Err(Error::Unsupported)
85        }
86    }
87
88    #[tokio::test]
89    async fn embed_delegates_to_client() {
90        let client = OkClient;
91        let result = embed(&client, "hello").await.unwrap();
92        assert_eq!(result, vec![0.1f32, 0.2, 0.3]);
93    }
94
95    #[tokio::test]
96    async fn embed_propagates_unsupported() {
97        let client = UnsupportedClient;
98        let result = embed(&client, "hello").await;
99        assert!(
100            matches!(result, Err(Error::Unsupported)),
101            "expected Err(Error::Unsupported), got: {result:?}"
102        );
103    }
104}