Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions embeddings/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum LibError {
HuggingFaceTokenInvalid,
RemoteHttpError {
status: u16,
message: Option<String>,
},
OnnxModelEvalFailed,
}
Expand Down Expand Up @@ -91,8 +92,12 @@ impl std::fmt::Display for LibError {
LibError::HuggingFaceTokenInvalid => {
write!(f, "Invalid or expired HuggingFace token")
}
LibError::RemoteHttpError { status } => {
write!(f, "HTTP error from remote model: status code {}", status)
LibError::RemoteHttpError { status, message } => {
if let Some(msg) = message {
write!(f, "HTTP error from remote model: status code {}: {}", status, msg)
} else {
write!(f, "HTTP error from remote model: status code {}", status)
}
}
LibError::OnnxModelEvalFailed => write!(f, "Failed to evaluate ONNX model"),
}
Expand Down
37 changes: 37 additions & 0 deletions embeddings/src/model/jina.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ impl JinaModel {

impl TextModel for JinaModel {
fn predict(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
// Pre-validate: filter out empty texts and check we have at least one non-empty input
let non_empty_count = texts.iter().filter(|t| !t.trim().is_empty()).count();
if non_empty_count == 0 {
return Err(Box::new(LibError::RemoteHttpError {
status: 400,
message: Some("all input texts are empty - at least one non-empty text is required for embeddings".to_string()),
}));
}

let url = self
.api_url
.as_deref()
Expand All @@ -136,10 +145,30 @@ impl TextModel for JinaModel {

let response_text = response.text().map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: None,
})?;

// Helper to extract error message from JSON response
let extract_error_message = |text: &str| -> Option<String> {
serde_json::from_str::<serde_json::Value>(text)
.ok()
.and_then(|body| {
// Try standard error.message format first
if let Some(msg) = body.get("error")?.get("message")?.as_str() {
return Some(msg.to_string());
}
// Try Jina's detail format
if let Some(detail) = body.get("detail")?.as_str() {
return Some(detail.to_string());
}
None
})
};

// Check HTTP status code first
if !status.is_success() {
let error_message = extract_error_message(&response_text);

// Try to parse JSON error response for more details
if let Ok(response_body) = serde_json::from_str::<serde_json::Value>(&response_text) {
if let Some(error) = response_body.get("error") {
Expand All @@ -166,6 +195,7 @@ impl TextModel for JinaModel {
}
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};

Expand Down Expand Up @@ -199,6 +229,7 @@ impl TextModel for JinaModel {
429 => LibError::RemoteRequestSendFailed,
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
}
};
Expand All @@ -217,6 +248,7 @@ impl TextModel for JinaModel {
429 => LibError::RemoteRequestSendFailed,
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};
return Err(Box::new(lib_error));
Expand All @@ -225,6 +257,7 @@ impl TextModel for JinaModel {
let response_body: serde_json::Value =
serde_json::from_str(&response_text).map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: Some("failed to parse successful response as JSON".to_string()),
})?;

// Check if there's an error in the response (shouldn't happen if status was success, but check anyway)
Expand All @@ -247,6 +280,7 @@ impl TextModel for JinaModel {
"rate_limit_exceeded" | "quota_exceeded" => LibError::RemoteRequestSendFailed,
_ => LibError::RemoteHttpError {
status: status_code,
message: error.get("message").and_then(|m| m.as_str()).map(|s| s.to_string()),
},
};

Expand All @@ -271,6 +305,7 @@ impl TextModel for JinaModel {
} else {
LibError::RemoteHttpError {
status: status_code,
message: Some(detail_str.to_string()),
}
};
return Err(Box::new(lib_error));
Expand All @@ -295,6 +330,7 @@ impl TextModel for JinaModel {
if embeddings.is_empty() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("API returned no embeddings in response".to_string()),
}));
}

Expand All @@ -306,6 +342,7 @@ impl TextModel for JinaModel {
if embedding.is_empty() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("API returned an empty embedding vector".to_string()),
}));
}
if embedding.len() != inferred_dim {
Expand Down
24 changes: 24 additions & 0 deletions embeddings/src/model/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ impl OpenAIModel {

impl TextModel for OpenAIModel {
fn predict(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
// Pre-validate: filter out empty texts and check we have at least one non-empty input
let non_empty_count = texts.iter().filter(|t| !t.trim().is_empty()).count();
if non_empty_count == 0 {
return Err(Box::new(LibError::RemoteHttpError {
status: 400,
message: Some("all input texts are empty - at least one non-empty text is required for embeddings".to_string()),
}));
}

let url = self
.api_url
.as_deref()
Expand All @@ -118,10 +127,20 @@ impl TextModel for OpenAIModel {

let response_text = response.text().map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: None,
})?;

// Helper to extract error message from JSON response
let extract_error_message = |text: &str| -> Option<String> {
serde_json::from_str::<serde_json::Value>(text)
.ok()
.and_then(|body| body.get("error")?.get("message")?.as_str().map(|s| s.to_string()))
};

// Check HTTP status code first
if !status.is_success() {
let error_message = extract_error_message(&response_text);

// Try to parse JSON error response for more details
if let Ok(response_body) = serde_json::from_str::<serde_json::Value>(&response_text) {
if let Some(error) = response_body.get("error") {
Expand All @@ -143,6 +162,7 @@ impl TextModel for OpenAIModel {
}
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};

Expand All @@ -160,6 +180,7 @@ impl TextModel for OpenAIModel {
429 => LibError::RemoteRequestSendFailed,
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};
return Err(Box::new(lib_error));
Expand All @@ -169,12 +190,14 @@ impl TextModel for OpenAIModel {
let response_body: serde_json::Value =
serde_json::from_str(&response_text).map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: Some("failed to parse successful response as JSON".to_string()),
})?;

let data_array = response_body["data"].as_array();
if data_array.is_none() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("response missing 'data' array".to_string()),
}));
}

Expand All @@ -196,6 +219,7 @@ impl TextModel for OpenAIModel {
if embeddings.is_empty() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("API returned no embeddings in response".to_string()),
}));
}

Expand Down
26 changes: 26 additions & 0 deletions embeddings/src/model/voyage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ impl VoyageModel {

impl TextModel for VoyageModel {
fn predict(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
// Pre-validate: filter out empty texts and check we have at least one non-empty input
let non_empty_count = texts.iter().filter(|t| !t.trim().is_empty()).count();
if non_empty_count == 0 {
return Err(Box::new(LibError::RemoteHttpError {
status: 400,
message: Some("all input texts are empty - at least one non-empty text is required for embeddings".to_string()),
}));
}

let url = self
.api_url
.as_deref()
Expand All @@ -123,10 +132,20 @@ impl TextModel for VoyageModel {

let response_text = response.text().map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: None,
})?;

// Helper to extract error message from JSON response
let extract_error_message = |text: &str| -> Option<String> {
serde_json::from_str::<serde_json::Value>(text)
.ok()
.and_then(|body| body.get("error")?.get("message")?.as_str().map(|s| s.to_string()))
};

// Check HTTP status code first
if !status.is_success() {
let error_message = extract_error_message(&response_text);

// Try to parse JSON error response for more details
if let Ok(response_body) = serde_json::from_str::<serde_json::Value>(&response_text) {
if let Some(error) = response_body.get("error") {
Expand All @@ -152,6 +171,7 @@ impl TextModel for VoyageModel {
}
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};

Expand All @@ -169,6 +189,7 @@ impl TextModel for VoyageModel {
429 => LibError::RemoteRequestSendFailed,
_ => LibError::RemoteHttpError {
status: status_code,
message: error_message,
},
};
return Err(Box::new(lib_error));
Expand All @@ -178,11 +199,13 @@ impl TextModel for VoyageModel {
let response_body: serde_json::Value =
serde_json::from_str(&response_text).map_err(|_| LibError::RemoteHttpError {
status: status_code,
message: Some("failed to parse successful response as JSON".to_string()),
})?;

if response_body.get("data").is_none() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("response missing 'data' field".to_string()),
}));
}

Expand All @@ -191,6 +214,7 @@ impl TextModel for VoyageModel {
if data_array.is_none() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("response 'data' is not an array".to_string()),
}));
}

Expand All @@ -212,6 +236,7 @@ impl TextModel for VoyageModel {
if embeddings.is_empty() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("API returned no embeddings in response".to_string()),
}));
}

Expand All @@ -223,6 +248,7 @@ impl TextModel for VoyageModel {
if embedding.is_empty() {
return Err(Box::new(LibError::RemoteHttpError {
status: status_code,
message: Some("API returned an empty embedding vector".to_string()),
}));
}
if embedding.len() != inferred_dim {
Expand Down
Loading