paladin-ports 0.5.1

Port trait definitions (hexagonal architecture contracts) for the Paladin framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
//! # Embedding Port - Text-to-Vector Embedding Interface
//!
//! Port trait defining how the application generates vector embeddings from text for semantic search.
//!
//! ## Purpose
//!
//! The Embedding Port provides a unified abstraction for converting text into dense vector
//! representations (embeddings). These embeddings enable semantic similarity search, allowing
//! the system to find conceptually similar content regardless of exact word matches. The port supports:
//!
//! - **Single Text Embedding**: Convert individual text strings to vectors
//! - **Batch Embedding**: Efficiently process multiple texts in one API call
//! - **Model Information**: Query embedding model name and dimension
//! - **Provider Abstraction**: Switch between OpenAI, local models, custom embedders
//!
//! By decoupling embedding generation from specific providers, Embedding Port enables:
//! - Switching between embedding models (OpenAI, Cohere, local Sentence Transformers)
//! - Testing with mock implementations
//! - Optimizing costs by choosing appropriate models per use case
//!
//! ## Hexagonal Architecture
//!
//! This is an **output port** in the application layer. It defines the interface for
//! text-to-vector conversion, allowing Paladin agents to perform semantic operations
//! without depending on specific embedding service implementations.
//!
//! **Adapter Implementations:**
//! - `OpenAIEmbeddingAdapter` - OpenAI embedding models (text-embedding-3-small, text-embedding-3-large)
//! - `LocalEmbeddingAdapter` - Local Sentence Transformers models
//! - `CohereEmbeddingAdapter` - Cohere embedding API
//! - `MockEmbeddingAdapter` - Testing with fixed vectors
//!
//! ## Thread Safety
//!
//! All implementations must be `Send + Sync` to support concurrent async operations.
//! Multiple Paladin agents may request embeddings simultaneously. Implementations should
//! handle concurrent requests efficiently (connection pooling, batching).
//!
//! ## Error Handling
//!
//! Operations return `Result<T, EmbeddingError>` with specific error variants for:
//! - Network failures (timeouts, connection issues)
//! - Rate limiting (API quota exhaustion)
//! - Invalid input (empty text, exceeds token limits)
//! - Provider errors (API key issues, model unavailable)
//!
//! See [`EmbeddingError`] for all error categories and handling strategies.
//!
//! ## Common Use Cases
//!
//! 1. **Semantic Search**: Generate query embeddings for similarity search in Sanctum/Garrison
//! 2. **RAG Systems**: Embed documents and queries for retrieval-augmented generation
//! 3. **Clustering**: Group similar content by embedding proximity
//! 4. **Deduplication**: Detect duplicate/similar content by high cosine similarity
//!
//! ## Examples
//!
//! ### Basic Text Embedding
//!
//! ```rust,no_run
//! use paladin::application::ports::output::embedding_port::EmbeddingPort;
//!
//! async fn embed_query(embedder: &dyn EmbeddingPort) -> Result<(), Box<dyn std::error::Error>> {
//!     let query = "What is the capital of France?";
//!     let embedding = embedder.embed_text(query).await?;
//!
//!     println!("Generated {}-dimensional embedding", embedding.dimension);
//!     println!("Using model: {}", embedding.model);
//!     println!("Vector preview: {:?}", &embedding.vector[..5]);
//!
//!     Ok(())
//! }
//! ```
//!
//! ### Batch Embedding for Efficiency
//!
//! ```rust,no_run
//! use paladin::application::ports::output::embedding_port::EmbeddingPort;
//!
//! async fn embed_documents(
//!     embedder: &dyn EmbeddingPort,
//!     documents: Vec<String>,
//! ) -> Result<(), Box<dyn std::error::Error>> {
//!     // Convert to &str for batch API
//!     let doc_refs: Vec<&str> = documents.iter().map(|s| s.as_str()).collect();
//!
//!     // Batch embedding (10-100x faster than individual calls)
//!     let embeddings = embedder.embed_batch(&doc_refs).await?;
//!
//!     println!("Embedded {} documents in one API call", embeddings.len());
//!
//!     // Process embeddings
//!     for (doc, emb) in documents.iter().zip(embeddings.iter()) {
//!         println!("Doc: {}... -> {}-dim vector",
//!             &doc.chars().take(30).collect::<String>(),
//!             emb.dimension);
//!     }
//!
//!     Ok(())
//! }
//! ```
//!
//! ### Semantic Similarity Calculation
//!
//! ```rust,no_run
//! use paladin::application::ports::output::embedding_port::EmbeddingPort;
//!
//! async fn calculate_similarity(
//!     embedder: &dyn EmbeddingPort,
//!     text1: &str,
//!     text2: &str,
//! ) -> Result<f32, Box<dyn std::error::Error>> {
//!     // Generate embeddings
//!     let emb1 = embedder.embed_text(text1).await?;
//!     let emb2 = embedder.embed_text(text2).await?;
//!
//!     // Calculate cosine similarity
//!     let dot_product: f32 = emb1.vector.iter()
//!         .zip(emb2.vector.iter())
//!         .map(|(a, b)| a * b)
//!         .sum();
//!
//!     let norm1: f32 = emb1.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
//!     let norm2: f32 = emb2.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
//!
//!     let similarity = dot_product / (norm1 * norm2);
//!     println!("Similarity: {:.3}", similarity);
//!
//!     Ok(similarity)
//! }
//! ```
//!
//! ## Implementation Notes
//!
//! ### Performance Considerations
//! - **Batch Operations**: Always use `embed_batch()` for multiple texts (10-100x faster)
//! - **Token Limits**: Check provider token limits (8K for OpenAI text-embedding-3-small)
//! - **Caching**: Cache embeddings for frequently embedded content
//! - **Connection Pooling**: Reuse HTTP connections for multiple requests
//! - **Rate Limiting**: Implement exponential backoff for rate limit errors
//!
//! ### Best Practices
//! 1. **Consistent Models**: Use same model for all embeddings in a collection
//! 2. **Normalization**: Normalize embeddings if using dot product instead of cosine similarity
//! 3. **Error Handling**: Always handle `RateLimited` errors with retry logic
//! 4. **Dimension Validation**: Verify embedding dimension matches expected value
//! 5. **Cost Optimization**: Choose appropriate model (small vs large) based on use case
//!
//! ### Common Pitfalls
//! - Embedding texts one-by-one instead of using batch API (slow, expensive)
//! - Mixing embeddings from different models (invalid similarity scores)
//! - Not handling rate limits (requests fail unnecessarily)
//! - Embedding very long texts without chunking (exceeds token limits)
//! - Not caching embeddings for repeated content (wasted API calls)
//!
//! ## Related Ports
//!
//! - [`SanctumPort`](crate::output::sanctum_port::SanctumPort) - Vector storage (stores embeddings from this port)
//! - [`LongTermGarrisonPort`](crate::output::garrison_port::LongTermGarrisonPort) - Garrison with embeddings (uses this port)
//! - [`LlmPort`](crate::output::llm_port::LlmPort) - LLM integration (complementary for RAG systems)
//!
//! ## See Also
//!
//! - [Application Ports](crate::application::ports)
//! - [OpenAI Embeddings Guide](https://platform.openai.com/docs/guides/embeddings)
//! - [Infrastructure Adapters](crate::infrastructure::adapters::embedding)

/// Errors that can occur during embedding generation.
///
/// All embedding operations return `Result<T, EmbeddingError>`. These errors cover
/// network failures, rate limiting, invalid input, and provider-specific issues.
///
/// # Error Categories
///
/// ## Transient Errors (Retryable)
/// - [`NetworkError`](Self::NetworkError) - May succeed on retry (connection issues, timeouts)
/// - [`RateLimited`](Self::RateLimited) - Retry after backoff period
///
/// ## Permanent Errors (Non-Retryable)
/// - [`InvalidInput`](Self::InvalidInput) - Text is empty or too long
/// - [`ProviderError`](Self::ProviderError) - API key invalid, model unavailable
///
/// # Examples
///
/// ## Error Handling with Retry
///
/// ```rust,no_run
/// use paladin::application::ports::output::embedding_port::{EmbeddingPort, EmbeddingError};
///
/// async fn embed_with_retry(
///     embedder: &dyn EmbeddingPort,
///     text: &str,
///     max_retries: u32,
/// ) -> Result<paladin::application::ports::output::embedding_port::Embedding, EmbeddingError> {
///     let mut attempts = 0;
///     loop {
///         match embedder.embed_text(text).await {
///             Ok(embedding) => return Ok(embedding),
///             Err(EmbeddingError::RateLimited(msg)) if attempts < max_retries => {
///                 attempts += 1;
///                 let wait_ms = 1000 * 2_u64.pow(attempts); // Exponential backoff
///                 eprintln!("Rate limited, waiting {}ms (attempt {})", wait_ms, attempts);
///                 tokio::time::sleep(tokio::time::Duration::from_millis(wait_ms)).await;
///             }
///             Err(EmbeddingError::NetworkError(e)) if attempts < max_retries => {
///                 attempts += 1;
///                 eprintln!("Network error, retrying: {}", e);
///                 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
///             }
///             Err(e) => return Err(e),
///         }
///     }
/// }
/// ```
///
/// ## Input Validation
///
/// ```rust,no_run
/// use paladin::application::ports::output::embedding_port::{EmbeddingPort, EmbeddingError};
///
/// async fn embed_with_validation(
///     embedder: &dyn EmbeddingPort,
///     text: &str,
/// ) -> Result<paladin::application::ports::output::embedding_port::Embedding, EmbeddingError> {
///     // Validate before embedding
///     if text.trim().is_empty() {
///         return Err(EmbeddingError::InvalidInput("Text is empty".to_string()));
///     }
///
///     let token_count = text.split_whitespace().count();
///     if token_count > 8000 {
///         return Err(EmbeddingError::InvalidInput(
///             format!("Text too long: {} tokens (max 8000)", token_count)
///         ));
///     }
///
///     embedder.embed_text(text).await
/// }
/// ```
#[derive(Debug, thiserror::Error)]
pub enum EmbeddingError {
    /// Network-related errors (timeouts, connection failures).
    ///
    /// **Retryable**: Yes (may be transient network issue)
    ///
    /// **Common Causes**:
    /// - Connection timeout to embedding service
    /// - DNS resolution failure
    /// - Network partition
    /// - TLS handshake failure
    ///
    /// **Recovery**: Retry with exponential backoff (500ms, 1s, 2s, 4s).
    #[error("Network error: {0}")]
    NetworkError(String),

    /// Rate limiting errors from the embedding provider.
    ///
    /// **Retryable**: Yes (retry after backoff period)
    ///
    /// **Common Causes**:
    /// - Exceeded requests per minute (RPM) limit
    /// - Exceeded tokens per minute (TPM) limit
    /// - Exceeded requests per day (RPD) limit
    ///
    /// **Recovery**: Retry with exponential backoff (1s, 2s, 4s, 8s). Check rate limit headers.
    #[error("Rate limited: {0}")]
    RateLimited(String),

    /// Invalid input provided to the embedding service.
    ///
    /// **Retryable**: No (input must be fixed)
    ///
    /// **Common Causes**:
    /// - Empty text string
    /// - Text exceeds token limit (8K for OpenAI text-embedding-3-small)
    /// - Invalid characters or encoding
    ///
    /// **Recovery**: Validate and fix input (trim whitespace, chunk long texts).
    #[error("Invalid input: {0}")]
    InvalidInput(String),

    /// Provider-specific errors (API key issues, model not found, etc.).
    ///
    /// **Retryable**: Usually no (configuration issue)
    ///
    /// **Common Causes**:
    /// - Invalid or expired API key
    /// - Model name doesn't exist
    /// - Insufficient permissions
    /// - Provider service outage
    ///
    /// **Recovery**: Check API key, model name, and provider status page.
    #[error("Provider error: {0}")]
    ProviderError(String),
}

/// A vector embedding with associated metadata.
///
/// Represents the result of converting text into a dense vector representation.
/// The vector enables semantic similarity calculations (cosine similarity, dot product).
///
/// # Fields
///
/// - `vector`: The dense vector representation (e.g., 1536 dimensions for OpenAI text-embedding-3-small)
/// - `model`: Model identifier used to generate this embedding
/// - `dimension`: Number of dimensions in the vector
/// - `token_count`: Optional token count (useful for billing estimation)
///
/// # Examples
///
/// ```rust
/// use paladin::application::ports::output::embedding_port::Embedding;
///
/// let embedding = Embedding {
///     vector: vec![0.1, 0.2, 0.3],
///     model: "text-embedding-3-small".to_string(),
///     dimension: 3,
///     token_count: Some(5),
/// };
///
/// assert_eq!(embedding.dimension, embedding.vector.len());
/// assert_eq!(embedding.model, "text-embedding-3-small");
/// ```
///
/// ## Similarity Calculation
///
/// ```rust
/// use paladin::application::ports::output::embedding_port::Embedding;
///
/// fn cosine_similarity(emb1: &Embedding, emb2: &Embedding) -> f32 {
///     let dot: f32 = emb1.vector.iter().zip(emb2.vector.iter()).map(|(a, b)| a * b).sum();
///     let norm1: f32 = emb1.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
///     let norm2: f32 = emb2.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
///     dot / (norm1 * norm2)
/// }
///
/// let emb1 = Embedding {
///     vector: vec![1.0, 0.0, 0.0],
///     model: "test".to_string(),
///     dimension: 3,
///     token_count: None,
/// };
///
/// let emb2 = Embedding {
///     vector: vec![0.8, 0.6, 0.0],
///     model: "test".to_string(),
///     dimension: 3,
///     token_count: None,
/// };
///
/// let similarity = cosine_similarity(&emb1, &emb2);
/// assert!((similarity - 0.8).abs() < 0.01);
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Embedding {
    /// The embedding vector
    pub vector: Vec<f32>,

    /// The model used to generate this embedding
    pub model: String,

    /// The dimension of the embedding vector
    pub dimension: usize,

    /// Optional token count for the embedded text
    pub token_count: Option<u32>,
}

/// Port trait for generating vector embeddings from text.\n///\n/// This trait provides a standardized interface for embedding providers,\n/// allowing the system to plug in different embedding services (OpenAI, Cohere,\n/// local models, etc.) without changing core business logic.\n///\n/// # Capabilities\n///\n/// - **Single Embedding**: Convert one text string to vector with [`embed_text`](Self::embed_text)\n/// - **Batch Embedding**: Process multiple texts efficiently with [`embed_batch`](Self::embed_batch)\n/// - **Model Info**: Query dimension and model name with [`dimension`](Self::dimension) / [`model_name`](Self::model_name)\n///\n/// # Thread Safety\n///\n/// All implementations must be `Send + Sync` to support concurrent async operations.\n/// Multiple Paladin agents may request embeddings simultaneously.\n///\n/// # Implementation Requirements\n///\n/// Implementations should:\n/// 1. Return normalized vectors if using dot product similarity (optional for cosine similarity)\n/// 2. Handle rate limiting with appropriate backoff\n/// 3. Support text chunking for inputs exceeding token limits\n/// 4. Reuse HTTP connections for efficiency (connection pooling)\n/// 5. Cache embeddings for frequently embedded content (optional but recommended)\n///\n/// # Examples\n///\n/// ## Embedding for Semantic Search\n///\n/// ```rust,no_run\n/// use paladin::application::ports::output::embedding_port::EmbeddingPort;\n/// use paladin::application::ports::output::sanctum_port::{SanctumPort, SanctumQuery};\n///\n/// async fn semantic_search(\n///     embedder: &dyn EmbeddingPort,\n///     sanctum: &dyn SanctumPort,\n///     query: &str,\n/// ) -> Result<(), Box<dyn std::error::Error>> {\n///     // Embed the query\n///     let embedding = embedder.embed_text(query).await?;\n///     \n///     // Search vector database\n///     let search_query = SanctumQuery::new(embedding.vector, 5);\n///     let results = sanctum.search(search_query).await?;\n///     \n///     println!(\"Found {} similar documents\", results.len());\n///     Ok(())\n/// }\n/// ```\n///\n/// ## Batch Processing for Performance\n///\n/// ```rust,no_run\n/// use paladin::application::ports::output::embedding_port::EmbeddingPort;\n///\n/// async fn process_documents(\n///     embedder: &dyn EmbeddingPort,\n///     documents: Vec<String>,\n/// ) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {\n///     // Process in batches of 100 for optimal performance\n///     let batch_size = 100;\n///     let mut all_embeddings = Vec::new();\n///     \n///     for chunk in documents.chunks(batch_size) {\n///         let refs: Vec<&str> = chunk.iter().map(|s| s.as_str()).collect();\n///         let embeddings = embedder.embed_batch(&refs).await?;\n///         all_embeddings.extend(embeddings.into_iter().map(|e| e.vector));\n///     }\n///     \n///     println!(\"Processed {} documents\", all_embeddings.len());\n///     Ok(all_embeddings)\n/// }\n/// ```\n///\n/// ## Custom Implementation Example\n///\n/// ```rust\n/// use paladin::application::ports::output::embedding_port::{EmbeddingPort, Embedding, EmbeddingError};\n/// use async_trait::async_trait;\n///\n/// struct MockEmbedder {\n///     dimension: usize,\n/// }\n///\n/// #[async_trait]\n/// impl EmbeddingPort for MockEmbedder {\n///     async fn embed_text(&self, text: &str) -> Result<Embedding, EmbeddingError> {\n///         if text.is_empty() {\n///             return Err(EmbeddingError::InvalidInput(\"Empty text\".to_string()));\n///         }\n///         \n///         // Generate deterministic mock vector\n///         let hash = text.len() as f32;\n///         let vector = vec![hash / 100.0; self.dimension];\n///         \n///         Ok(Embedding {\n///             vector,\n///             model: \"mock-model\".to_string(),\n///             dimension: self.dimension,\n///             token_count: Some(text.split_whitespace().count() as u32),\n///         })\n///     }\n///     \n///     async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Embedding>, EmbeddingError> {\n///         let mut embeddings = Vec::new();\n///         for text in texts {\n///             embeddings.push(self.embed_text(text).await?);\n///         }\n///         Ok(embeddings)\n///     }\n///     \n///     fn dimension(&self) -> usize {\n///         self.dimension\n///     }\n///     \n///     fn model_name(&self) -> &str {\n///         \"mock-model\"\n///     }\n/// }\n/// ```\n///\n/// # Implementation Notes\n///\n/// ## Provider Selection\n///\n/// Choose based on requirements:\n/// - **OpenAI text-embedding-3-small**: Best cost/performance (1536 dim, $0.02/1M tokens)\n/// - **OpenAI text-embedding-3-large**: Highest quality (3072 dim, $0.13/1M tokens)\n/// - **Cohere embed-english-v3.0**: Good for English (1024 dim)\n/// - **Local Sentence Transformers**: Free, private, offline (384-768 dim)\n///\n/// ## Performance Optimization\n///\n/// ```rust,ignore\n/// // Good: Batch embedding (10-100x faster)\n/// let texts = vec![\"text1\", \"text2\", \"text3\"];\n/// let embeddings = embedder.embed_batch(&texts).await?;\n///\n/// // Avoid: Individual calls in loop\n/// for text in texts {\n///     let embedding = embedder.embed_text(text).await?; // Slow!\n/// }\n///\n/// // Good: Chunk long documents\n/// fn chunk_text(text: &str, max_tokens: usize) -> Vec<String> {\n///     text.split_whitespace()\n///         .collect::<Vec<_>>()\n///         .chunks(max_tokens)\n///         .map(|chunk| chunk.join(\" \"))\n///         .collect()\n/// }\n///\n/// // Good: Cache frequently embedded content\n/// use std::collections::HashMap;\n/// let mut cache: HashMap<String, Embedding> = HashMap::new();\n/// if let Some(cached) = cache.get(text) {\n///     return Ok(cached.clone());\n/// }\n/// ```\n///\n/// ## Error Handling Best Practices\n///\n/// 1. **Rate Limits**: Implement exponential backoff (1s, 2s, 4s, 8s)\n/// 2. **Network Errors**: Retry up to 3 times with 500ms delay\n/// 3. **Invalid Input**: Validate before calling (empty check, length check)\n/// 4. **Provider Errors**: Log and alert (indicates configuration issue)\n///\n/// ## Token Limits by Provider\n///\n/// - OpenAI text-embedding-3-small: 8,191 tokens\n/// - OpenAI text-embedding-3-large: 8,191 tokens\n/// - Cohere embed-english-v3.0: 512 tokens\n/// - Local models: Varies (typically 256-512)\n///\n/// ## Cost Estimation\n///\n/// ```rust,ignore\n/// // OpenAI pricing (as of 2024)\n/// fn estimate_cost(token_count: u32, model: &str) -> f64 {\n///     let cost_per_1m = match model {\n///         \"text-embedding-3-small\" => 0.02,\n///         \"text-embedding-3-large\" => 0.13,\n///         _ => 0.0,\n///     };\n///     (token_count as f64 / 1_000_000.0) * cost_per_1m\n/// }\n/// ```\n///\n/// # Common Pitfalls\n///\n/// - Not using batch API for multiple texts (10-100x slower)\n/// - Mixing embeddings from different models (invalid similarity)\n/// - Not handling rate limits (requests fail unnecessarily)\n/// - Embedding without input validation (wasted API calls)\n/// - Not caching embeddings (repeated costs)\n///\n/// # See Also\n///\n/// - [`Embedding`] - Vector embedding data structure\n/// - [`EmbeddingError`] - Error types and recovery strategies\n/// - [`SanctumPort`] - Vector storage (uses embeddings)\n/// - [`LongTermGarrisonPort`] - Memory with embeddings
#[async_trait::async_trait]
pub trait EmbeddingPort: Send + Sync {
    /// Generate an embedding for a single text string
    ///
    /// # Arguments
    ///
    /// * `text` - The text to embed
    ///
    /// # Returns
    ///
    /// Returns an `Embedding` containing the vector and metadata, or an `EmbeddingError`
    ///
    /// # Errors
    ///
    /// * `NetworkError` - If there's a network issue connecting to the provider
    /// * `RateLimited` - If the rate limit has been exceeded
    /// * `InvalidInput` - If the text is empty or exceeds size limits
    /// * `ProviderError` - If there's an API key issue or other provider-specific error
    async fn embed_text(&self, text: &str) -> Result<Embedding, EmbeddingError>;

    /// Generate embeddings for multiple text strings in a batch
    ///
    /// This is more efficient than calling `embed_text` multiple times,
    /// as it can leverage the provider's batch API.
    ///
    /// # Arguments
    ///
    /// * `texts` - A slice of text strings to embed
    ///
    /// # Returns
    ///
    /// Returns a vector of `Embedding` objects in the same order as the input,
    /// or an `EmbeddingError` if the entire batch fails
    ///
    /// # Errors
    ///
    /// Same error types as `embed_text`, plus provider-specific batch size limits
    async fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Embedding>, EmbeddingError>;

    /// Get the dimension of embeddings produced by this provider
    ///
    /// # Returns
    ///
    /// The number of dimensions in the embedding vector (e.g., 1536, 3072)
    fn dimension(&self) -> usize;

    /// Get the name of the model used by this provider
    ///
    /// # Returns
    ///
    /// The model identifier (e.g., "text-embedding-3-small")
    fn model_name(&self) -> &str;
}