use std::sync::Arc;
use serde_json::{Value, json};
use crate::chat::{failure_tokens, resolve_model};
use crate::config::Config;
use crate::cost::{Cost, Tier};
use crate::error::{Error, Result};
use crate::message::{Operation, UsageEntry, UsageStatus};
use crate::model::Model;
use crate::models;
use crate::protocols::int;
use crate::providers::Provider;
use crate::tokens::Tokens;
use crate::transport::Connection;
#[derive(Debug, Clone, PartialEq)]
pub struct RerankResult {
pub index: usize,
pub document: String,
pub score: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Rerank {
pub results: Vec<RerankResult>,
pub model: String,
pub raw: Value,
pub usage_entries: Vec<UsageEntry>,
input_tokens: Option<i64>,
reported_cost: Option<f64>,
pub(crate) model_info: Option<Model>,
}
impl Rerank {
pub fn new(results: Vec<RerankResult>, model: &str, input_tokens: Option<i64>) -> Rerank {
Rerank {
results,
model: model.to_string(),
raw: Value::Null,
usage_entries: Vec::new(),
input_tokens,
reported_cost: None,
model_info: None,
}
}
pub fn tokens(&self) -> Tokens {
if !self.usage_entries.is_empty() {
return Tokens::aggregate(self.usage_entries.iter().map(|e| &e.tokens));
}
Tokens {
input: self.input_tokens,
reported_cost: self.reported_cost,
..Default::default()
}
}
pub fn cost(&self) -> Cost {
if !self.usage_entries.is_empty() {
let complete = self.usage_entries.iter().all(UsageEntry::cost_available);
return Cost::aggregate(self.usage_entries.iter().map(|e| &e.cost), complete);
}
embeddings_cost(&self.tokens(), self.model_info().as_ref())
}
pub fn model_info(&self) -> Option<Model> {
self.model_info
.clone()
.or_else(|| models::models().find(&self.model, None).ok())
}
}
pub(crate) fn embeddings_cost(tokens: &Tokens, model: Option<&Model>) -> Cost {
let Some(model) = model else {
return Cost::new(tokens, None, Tier::Standard);
};
let Some(embeddings) = model.pricing.embeddings.as_ref() else {
return Cost::new(tokens, Some(model), Tier::Standard);
};
let mut priced = model.clone();
let mut text = priced.pricing.text_tokens.clone().unwrap_or_default();
let mut standard = text.standard.clone().unwrap_or_default();
standard.input_per_million = embeddings.input().or(standard.input_per_million);
standard.output_per_million = embeddings.output().or(standard.output_per_million);
text.standard = Some(standard);
priced.pricing.text_tokens = Some(text);
Cost::new(tokens, Some(&priced), Tier::Standard)
}
#[derive(Default)]
pub struct RerankOptions<'a> {
pub provider: Option<&'a str>,
pub assume_model_exists: bool,
pub top_n: Option<i64>,
pub provider_options: Value,
pub config: Option<Arc<Config>>,
pub metadata: Option<Value>,
pub owner: Option<crate::accounting::UsageOwner>,
}
pub async fn rerank(
query: &str,
documents: &[&str],
model: &str,
options: RerankOptions<'_>,
) -> Result<Rerank> {
let config = options.config.clone().unwrap_or_else(crate::config);
let (model, provider) = resolve_model(model, options.provider, options.assume_model_exists)?;
let owner = options.owner.clone();
let mut event = crate::instrumentation::Event::start(&config, "rerank.rust_llm", || {
let empty = Tokens::default();
crate::instrumentation::payload([
("provider", provider.slug().into()),
("provider_class", provider.display().into()),
("model", model.id.clone().into()),
("query", query.into()),
("document_count", documents.len().into()),
("top_n", options.top_n.into()),
("provider_options", options.provider_options.clone()),
(
"metadata",
crate::instrumentation::metadata(&options.metadata),
),
("tokens", crate::instrumentation::tokens_h(&empty)),
(
"cost",
crate::instrumentation::cost_h(&embeddings_cost(&empty, Some(&model))),
),
])
});
let result = event
.instrument(crate::accounting::owned_by(
owner,
rerank_inner(query, documents, options, config.clone(), model, provider),
))
.await;
if let Ok(r) = &result {
crate::accounting::report(&config, &r.usage_entries).await;
event.set("result", || {
json!({ "model": r.model, "results": r.results.iter().map(|x| json!({ "index": x.index, "score": x.score })).collect::<Vec<_>>() })
});
event.set("tokens", || crate::instrumentation::tokens_h(&r.tokens()));
event.set("cost", || crate::instrumentation::cost_h(&r.cost()));
}
event.finish(result.as_ref().err());
result
}
async fn rerank_inner(
query: &str,
documents: &[&str],
options: RerankOptions<'_>,
config: Arc<Config>,
model: Model,
provider: Provider,
) -> Result<Rerank> {
let RerankOptions {
top_n,
provider_options,
..
} = options;
provider.ensure_configured(&config)?;
if !matches!(provider, Provider::OpenRouter | Provider::GPUStack) {
return Err(Error::Api(
format!("{} doesn't support reranking", provider.display()),
None,
));
}
let connection = Connection::new(provider, config.clone())?;
let payload = render_payload(query, documents, &model.id, top_n, &provider_options);
let mut retried: Vec<Tokens> = Vec::new();
let mut on_attempt = |previous: Option<&Error>| {
if let Some(e) = previous {
retried.push(failure_tokens(e, None));
}
};
let raw = connection
.post("rerank", &payload, &[], &mut on_attempt)
.await?;
let mut result = parse_response(raw.body, &model.id, documents)?;
let entry = |status, tokens: Tokens, cost: Option<Cost>| UsageEntry {
id: UsageEntry::next_id(),
owner: crate::accounting::usage_owner(),
operation: Operation::Rerank,
provider: provider.slug().into(),
model: model.id.clone(),
status,
cost: cost
.filter(|c| c.total().is_some())
.unwrap_or_else(|| embeddings_cost(&tokens, Some(&model))),
tokens,
};
let mut entries: Vec<UsageEntry> = retried
.into_iter()
.map(|t| entry(UsageStatus::Failed, t, None))
.collect();
entries.push(entry(
UsageStatus::Succeeded,
result.tokens(),
Some(result.cost()),
));
result.usage_entries = entries;
Ok(result)
}
fn render_payload(
query: &str,
documents: &[&str],
model: &str,
top_n: Option<i64>,
provider_options: &Value,
) -> Value {
let mut payload = json!({ "model": model, "query": query, "documents": documents });
if let Some(top_n) = top_n {
payload["top_n"] = top_n.into();
}
if let (Some(p), Some(options)) = (payload.as_object_mut(), provider_options.as_object()) {
p.extend(options.clone());
}
payload
}
fn parse_response(data: Value, model: &str, documents: &[&str]) -> Result<Rerank> {
let mut results = Vec::new();
for result in data
.get("results")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let index = result
.get("index")
.and_then(Value::as_u64)
.map(|i| i as usize)
.filter(|i| *i < documents.len())
.ok_or_else(|| {
Error::Api(
"Rerank endpoint returned an invalid document index".into(),
None,
)
})?;
let document = match result.get("document") {
Some(Value::Object(d)) => d.get("text").and_then(Value::as_str).map(str::to_string),
Some(Value::String(s)) => Some(s.clone()),
_ => None,
};
results.push(RerankResult {
index,
document: document.unwrap_or_else(|| documents[index].to_string()),
score: result.get("relevance_score").and_then(Value::as_f64),
});
}
let usage = data.get("usage").cloned().unwrap_or_else(|| json!({}));
Ok(Rerank {
results,
model: data
.get("model")
.and_then(Value::as_str)
.unwrap_or(model)
.to_string(),
input_tokens: int(usage.get("total_tokens")),
reported_cost: usage.get("cost").and_then(Value::as_f64),
raw: data,
usage_entries: Vec::new(),
model_info: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_the_jina_style_rerank_request() {
let payload = render_payload(
"what is ruby",
&["a", "b"],
"voyageai/rerank-2.5-lite",
Some(2),
&Value::Null,
);
assert_eq!(
payload,
json!({ "model": "voyageai/rerank-2.5-lite", "query": "what is ruby", "documents": ["a", "b"], "top_n": 2 })
);
}
#[test]
fn an_out_of_range_index_is_an_error() {
let err = parse_response(
json!({ "results": [{ "index": 2, "relevance_score": 0.1 }] }),
"m",
&["a", "b"],
)
.unwrap_err();
assert!(err.to_string().contains("invalid document index"));
}
#[test]
fn rejects_a_negative_document_index() {
let err = parse_response(
json!({ "model": "voyageai/rerank-2.5-lite", "results": [{ "index": -1, "relevance_score": 0.9 }] }),
"voyageai/rerank-2.5-lite",
&["doc0", "doc1", "doc2"],
)
.unwrap_err();
assert!(
matches!(&err, Error::Api(m, _) if m.contains("invalid document index")),
"{err:?}"
);
}
}