sqlite_graphrag/embedding_api/
client.rs1use super::error::EmbedError;
7use super::mrl::{model_default_input_type, mrl_wire_dimensions};
8use super::wire::{EmbeddingInput, EmbeddingRequest};
9use super::{
10 OpenRouterClient, DEFAULT_CONNECT_TIMEOUT_SECS, DEFAULT_EMBED_HTTP_BATCH_SIZE,
11 DEFAULT_TIMEOUT_SECS,
12};
13use crate::constants::DEFAULT_OPENROUTER_EMBEDDINGS_URL;
14use crate::errors::AppError;
15use secrecy::SecretBox;
16use std::time::Duration;
17
18impl OpenRouterClient {
19 pub fn new(
26 api_key: SecretBox<String>,
27 model: String,
28 dim: usize,
29 timeout_secs: u64,
30 ) -> Result<Self, AppError> {
31 let base_url =
32 crate::runtime_config::openrouter_embeddings_url(DEFAULT_OPENROUTER_EMBEDDINGS_URL);
33 Self::new_with_base_url(api_key, model, dim, timeout_secs, base_url)
34 }
35
36 pub fn new_with_base_url(
40 api_key: SecretBox<String>,
41 model: String,
42 dim: usize,
43 timeout_secs: u64,
44 base_url: String,
45 ) -> Result<Self, AppError> {
46 let timeout_secs = if timeout_secs == 0 {
47 DEFAULT_TIMEOUT_SECS
48 } else {
49 timeout_secs
50 };
51 let client = reqwest::Client::builder()
52 .timeout(Duration::from_secs(timeout_secs))
53 .connect_timeout(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS))
54 .user_agent(concat!("sqlite-graphrag/", env!("CARGO_PKG_VERSION")))
55 .build()
56 .map_err(|e| {
57 AppError::Embedding(crate::i18n::validation::embedding_http_client_build_failed(
58 e,
59 ))
60 })?;
61
62 let default_input_type = model_default_input_type(&model);
63
64 Ok(Self {
65 client,
66 api_key,
67 model,
68 dim,
69 default_input_type,
70 base_url,
71 })
72 }
73
74 #[cfg(test)]
78 pub(super) fn new_with_url(
79 api_key: SecretBox<String>,
80 model: String,
81 dim: usize,
82 timeout_secs: u64,
83 base_url: String,
84 ) -> Result<Self, AppError> {
85 Self::new_with_base_url(api_key, model, dim, timeout_secs, base_url)
86 }
87
88 pub fn default_input_type(&self) -> Option<&'static str> {
90 self.default_input_type
91 }
92
93 pub async fn embed_single(
95 &self,
96 text: &str,
97 input_type: Option<&str>,
98 ) -> Result<Vec<f32>, EmbedError> {
99 crate::memory_guard::check_embedding_input_size(text)?;
103
104 let request = EmbeddingRequest {
105 model: &self.model,
106 input: EmbeddingInput::Single(text),
107 dimensions: mrl_wire_dimensions(&self.model, self.dim),
108 encoding_format: "float",
109 input_type,
110 };
111
112 let response = self.execute_with_retry(&request).await?;
113
114 let embedding = response
115 .data
116 .into_iter()
117 .next()
118 .ok_or_else(|| {
119 AppError::Embedding(
120 crate::i18n::validation::embedding_empty_response_from_openrouter(),
121 )
122 })?
123 .embedding;
124
125 Ok(self.truncate_embedding(embedding)?)
126 }
127
128 pub async fn embed_batch(
130 &self,
131 texts: &[&str],
132 input_type: Option<&str>,
133 ) -> Result<Vec<Vec<f32>>, EmbedError> {
134 if texts.is_empty() {
135 return Ok(Vec::new());
136 }
137
138 for text in texts {
142 crate::memory_guard::check_embedding_input_size(text)?;
143 }
144
145 let mut all = Vec::with_capacity(texts.len());
146
147 let batch_size = crate::runtime_config::embedding_batch_size(DEFAULT_EMBED_HTTP_BATCH_SIZE);
148 for chunk in texts.chunks(batch_size) {
149 let request = EmbeddingRequest {
150 model: &self.model,
151 input: EmbeddingInput::Batch(chunk.to_vec()),
152 dimensions: mrl_wire_dimensions(&self.model, self.dim),
153 encoding_format: "float",
154 input_type,
155 };
156
157 let response = self.execute_with_retry(&request).await?;
158
159 if response.data.len() != chunk.len() {
160 return Err(AppError::Embedding(
161 crate::i18n::validation::embedding_expected_count(
162 chunk.len(),
163 response.data.len(),
164 ),
165 )
166 .into());
167 }
168
169 let mut sorted = response.data;
170 sorted.sort_by_key(|d| d.index);
171
172 for d in sorted {
173 all.push(self.truncate_embedding(d.embedding)?);
174 }
175 }
176
177 Ok(all)
178 }
179}