Skip to main content

openai_protocol/
rerank.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use validator::Validate;
6
7use super::common::{default_true, GenerationRequest, StringOrArray, UsageInfo};
8
9fn default_rerank_object() -> String {
10    "rerank".to_string()
11}
12
13fn current_timestamp() -> i64 {
14    std::time::SystemTime::now()
15        .duration_since(std::time::UNIX_EPOCH)
16        .unwrap_or_else(|_| std::time::Duration::from_secs(0))
17        .as_secs() as i64
18}
19
20// ============================================================================
21// Rerank API
22// ============================================================================
23
24#[derive(Debug, Clone, Deserialize, Serialize, Validate, schemars::JsonSchema)]
25#[validate(schema(function = "validate_rerank_request"))]
26pub struct RerankRequest {
27    /// The query text to rank documents against
28    #[validate(custom(function = "validate_query"))]
29    pub query: String,
30
31    /// List of documents to be ranked
32    #[validate(custom(function = "validate_documents"))]
33    pub documents: Vec<String>,
34
35    /// Model to use for reranking
36    pub model: String,
37
38    /// Maximum number of documents to return (optional)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    #[validate(range(min = 1))]
41    pub top_k: Option<usize>,
42
43    /// Whether to return documents in addition to scores
44    #[serde(default = "default_true")]
45    pub return_documents: bool,
46
47    // SGLang specific extensions
48    /// Request ID for tracking
49    pub rid: Option<StringOrArray>,
50
51    /// User identifier
52    pub user: Option<String>,
53}
54
55impl GenerationRequest for RerankRequest {
56    fn rid(&self) -> Option<&str> {
57        self.rid.as_ref().and_then(StringOrArray::first)
58    }
59
60    fn get_model(&self) -> Option<&str> {
61        Some(&self.model)
62    }
63
64    fn is_stream(&self) -> bool {
65        false // Reranking doesn't support streaming
66    }
67
68    fn extract_text_for_routing(&self) -> String {
69        self.query.clone()
70    }
71}
72
73impl super::validated::Normalizable for RerankRequest {
74    // Use default no-op normalization
75}
76
77// ============================================================================
78// Validation Functions
79// ============================================================================
80
81/// Validates that the query is not empty
82fn validate_query(query: &str) -> Result<(), validator::ValidationError> {
83    if query.trim().is_empty() {
84        return Err(validator::ValidationError::new("query cannot be empty"));
85    }
86    Ok(())
87}
88
89/// Validates that the documents list is not empty
90fn validate_documents(documents: &[String]) -> Result<(), validator::ValidationError> {
91    if documents.is_empty() {
92        return Err(validator::ValidationError::new(
93            "documents list cannot be empty",
94        ));
95    }
96    Ok(())
97}
98
99/// Schema-level validation for cross-field dependencies
100#[expect(
101    clippy::unnecessary_wraps,
102    reason = "validator crate requires Result return type"
103)]
104fn validate_rerank_request(req: &RerankRequest) -> Result<(), validator::ValidationError> {
105    // Validate top_k if specified
106    if let Some(k) = req.top_k {
107        if k > req.documents.len() {
108            tracing::warn!(
109                "top_k ({}) is greater than number of documents ({})",
110                k,
111                req.documents.len()
112            );
113        }
114    }
115    Ok(())
116}
117
118impl RerankRequest {
119    /// Get the effective top_k value
120    pub fn effective_top_k(&self) -> usize {
121        self.top_k.unwrap_or(self.documents.len())
122    }
123}
124
125/// Individual rerank result
126#[serde_with::skip_serializing_none]
127#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
128pub struct RerankResult {
129    /// Relevance score for the document
130    pub score: f32,
131
132    /// The document text (if return_documents was true)
133    pub document: Option<String>,
134
135    /// Original index of the document in the request
136    pub index: usize,
137
138    /// Additional metadata about the ranking
139    pub meta_info: Option<HashMap<String, Value>>,
140}
141
142/// Rerank response containing sorted results
143#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
144pub struct RerankResponse {
145    /// Ranked results sorted by score (highest first)
146    pub results: Vec<RerankResult>,
147
148    /// Model used for reranking
149    pub model: String,
150
151    /// Usage information
152    pub usage: Option<UsageInfo>,
153
154    /// Response object type
155    #[serde(default = "default_rerank_object")]
156    pub object: String,
157
158    /// Response ID
159    pub id: Option<StringOrArray>,
160
161    /// Creation timestamp
162    pub created: i64,
163}
164
165impl RerankResponse {
166    /// Create a new RerankResponse with the given results and model
167    pub fn new(
168        results: Vec<RerankResult>,
169        model: String,
170        request_id: Option<StringOrArray>,
171    ) -> Self {
172        RerankResponse {
173            results,
174            model,
175            usage: None,
176            object: default_rerank_object(),
177            id: request_id,
178            created: current_timestamp(),
179        }
180    }
181
182    /// Apply top_k limit to results
183    pub fn apply_top_k(&mut self, k: usize) {
184        self.results.truncate(k);
185    }
186
187    /// Drop documents from results (when return_documents is false)
188    pub fn drop_documents(&mut self) {
189        for result in &mut self.results {
190            result.document = None;
191        }
192    }
193}
194
195/// V1 API compatibility format for rerank requests
196/// Matches Python's V1RerankReqInput
197#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
198pub struct V1RerankReqInput {
199    pub query: String,
200    pub documents: Vec<String>,
201}
202
203/// Convert V1RerankReqInput to RerankRequest
204impl From<V1RerankReqInput> for RerankRequest {
205    fn from(v1: V1RerankReqInput) -> Self {
206        RerankRequest {
207            query: v1.query,
208            documents: v1.documents,
209            model: super::UNKNOWN_MODEL_ID.to_string(),
210            top_k: None,
211            return_documents: true,
212            rid: None,
213            user: None,
214        }
215    }
216}