semantic bufo search
find-bufo.com
bufo
1//! voyage AI embedding implementation
2//!
3//! implements the `Embedder` trait for voyage's multimodal-3 model.
4
5use crate::providers::{Embedder, EmbeddingError};
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8
9const VOYAGE_API_URL: &str = "https://api.voyageai.com/v1/multimodalembeddings";
10const VOYAGE_MODEL: &str = "voyage-multimodal-3";
11
12#[derive(Debug, Serialize)]
13struct VoyageRequest {
14 inputs: Vec<MultimodalInput>,
15 model: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 input_type: Option<String>,
18}
19
20#[derive(Debug, Serialize)]
21struct MultimodalInput {
22 content: Vec<ContentSegment>,
23}
24
25#[derive(Debug, Serialize)]
26#[serde(tag = "type", rename_all = "snake_case")]
27enum ContentSegment {
28 Text { text: String },
29}
30
31#[derive(Debug, Deserialize)]
32struct VoyageResponse {
33 data: Vec<VoyageEmbeddingData>,
34}
35
36#[derive(Debug, Deserialize)]
37struct VoyageEmbeddingData {
38 embedding: Vec<f32>,
39}
40
41/// voyage AI multimodal embedding client
42///
43/// uses the voyage-multimodal-3 model which produces 1024-dimensional vectors.
44/// designed for early fusion of text and image content.
45#[derive(Clone)]
46pub struct VoyageEmbedder {
47 client: Client,
48 api_key: String,
49}
50
51impl VoyageEmbedder {
52 pub fn new(api_key: String) -> Self {
53 Self {
54 client: Client::new(),
55 api_key,
56 }
57 }
58}
59
60impl Embedder for VoyageEmbedder {
61 async fn embed(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
62 let request = VoyageRequest {
63 inputs: vec![MultimodalInput {
64 content: vec![ContentSegment::Text {
65 text: text.to_string(),
66 }],
67 }],
68 model: VOYAGE_MODEL.to_string(),
69 input_type: Some("query".to_string()),
70 };
71
72 let response = self
73 .client
74 .post(VOYAGE_API_URL)
75 .header("Authorization", format!("Bearer {}", self.api_key))
76 .json(&request)
77 .send()
78 .await?;
79
80 if !response.status().is_success() {
81 let status = response.status().as_u16();
82 let body = response.text().await.unwrap_or_default();
83 return Err(EmbeddingError::Api { status, body });
84 }
85
86 let voyage_response: VoyageResponse = response.json().await.map_err(|e| {
87 EmbeddingError::Other(anyhow::anyhow!("failed to parse response: {}", e))
88 })?;
89
90 voyage_response
91 .data
92 .into_iter()
93 .next()
94 .map(|d| d.embedding)
95 .ok_or(EmbeddingError::EmptyResponse)
96 }
97
98 fn name(&self) -> &'static str {
99 "voyage-multimodal-3"
100 }
101}
102