Skip to main content

dakera_client/
types.rs

1//! Types for the Dakera client SDK
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6// ============================================================================
7// Retry & Timeout Configuration
8// ============================================================================
9
10/// Configuration for request retry behavior with exponential backoff.
11#[derive(Debug, Clone)]
12pub struct RetryConfig {
13    /// Maximum number of retry attempts (default: 3).
14    pub max_retries: u32,
15    /// Base delay before the first retry (default: 100ms).
16    pub base_delay: std::time::Duration,
17    /// Maximum delay between retries (default: 60s).
18    pub max_delay: std::time::Duration,
19    /// Whether to add random jitter to backoff delay (default: true).
20    pub jitter: bool,
21}
22
23impl Default for RetryConfig {
24    fn default() -> Self {
25        Self {
26            max_retries: 3,
27            base_delay: std::time::Duration::from_millis(100),
28            max_delay: std::time::Duration::from_secs(60),
29            jitter: true,
30        }
31    }
32}
33
34// ============================================================================
35// OPS-1: Rate-Limit Headers
36// ============================================================================
37
38/// Rate-limit and quota headers present on every API response (OPS-1).
39///
40/// Fields are `None` when the server does not include the header (e.g.
41/// non-namespaced endpoints where quota does not apply).
42#[derive(Debug, Clone, Default)]
43pub struct RateLimitHeaders {
44    /// `X-RateLimit-Limit` — max requests allowed in the current window.
45    pub limit: Option<u64>,
46    /// `X-RateLimit-Remaining` — requests left in the current window.
47    pub remaining: Option<u64>,
48    /// `X-RateLimit-Reset` — Unix timestamp (seconds) when the window resets.
49    pub reset: Option<u64>,
50    /// `X-Quota-Used` — namespace vectors / storage consumed.
51    pub quota_used: Option<u64>,
52    /// `X-Quota-Limit` — namespace quota ceiling.
53    pub quota_limit: Option<u64>,
54}
55
56impl RateLimitHeaders {
57    /// Parse rate-limit headers from a `reqwest::Response`.
58    pub fn from_response(response: &reqwest::Response) -> Self {
59        let headers = response.headers();
60        fn parse(h: &reqwest::header::HeaderMap, name: &str) -> Option<u64> {
61            h.get(name)
62                .and_then(|v| v.to_str().ok())
63                .and_then(|s| s.parse().ok())
64        }
65        Self {
66            limit: parse(headers, "X-RateLimit-Limit"),
67            remaining: parse(headers, "X-RateLimit-Remaining"),
68            reset: parse(headers, "X-RateLimit-Reset"),
69            quota_used: parse(headers, "X-Quota-Used"),
70            quota_limit: parse(headers, "X-Quota-Limit"),
71        }
72    }
73}
74
75// ============================================================================
76// Health & Status Types
77// ============================================================================
78
79/// Health check response
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct HealthResponse {
82    /// Overall health status
83    pub healthy: bool,
84    /// Service version
85    pub version: Option<String>,
86    /// Uptime in seconds
87    pub uptime_seconds: Option<u64>,
88    /// Git commit SHA baked into the binary at build time. Present since server v0.11.84.
89    pub build_sha: Option<String>,
90}
91
92/// Readiness check response
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ReadinessResponse {
95    /// Is the service ready to accept requests
96    pub ready: bool,
97    /// Component status details
98    pub components: Option<HashMap<String, bool>>,
99}
100
101// ============================================================================
102// Namespace Types
103// ============================================================================
104
105/// Namespace information
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct NamespaceInfo {
108    /// Namespace name
109    #[serde(alias = "namespace")]
110    pub name: String,
111    /// Number of vectors in the namespace
112    #[serde(default)]
113    pub vector_count: u64,
114    /// Vector dimensions
115    #[serde(alias = "dimension")]
116    pub dimensions: Option<u32>,
117    /// Index type used
118    pub index_type: Option<String>,
119    /// Whether the namespace was newly created (from PUT/configure response)
120    #[serde(default)]
121    pub created: Option<bool>,
122}
123
124/// List namespaces response
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ListNamespacesResponse {
127    /// List of namespace names
128    pub namespaces: Vec<String>,
129}
130
131// ============================================================================
132// Vector Types
133// ============================================================================
134
135/// A vector with ID and optional metadata
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct Vector {
138    /// Unique vector identifier
139    pub id: String,
140    /// Vector values (embeddings)
141    pub values: Vec<f32>,
142    /// Optional metadata
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub metadata: Option<HashMap<String, serde_json::Value>>,
145}
146
147impl Vector {
148    /// Create a new vector with just ID and values
149    pub fn new(id: impl Into<String>, values: Vec<f32>) -> Self {
150        Self {
151            id: id.into(),
152            values,
153            metadata: None,
154        }
155    }
156
157    /// Create a new vector with metadata
158    pub fn with_metadata(
159        id: impl Into<String>,
160        values: Vec<f32>,
161        metadata: HashMap<String, serde_json::Value>,
162    ) -> Self {
163        Self {
164            id: id.into(),
165            values,
166            metadata: Some(metadata),
167        }
168    }
169}
170
171/// Upsert vectors request
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct UpsertRequest {
174    /// Vectors to upsert
175    pub vectors: Vec<Vector>,
176}
177
178impl UpsertRequest {
179    /// Create a new upsert request with a single vector
180    pub fn single(vector: Vector) -> Self {
181        Self {
182            vectors: vec![vector],
183        }
184    }
185
186    /// Create a new upsert request with multiple vectors
187    pub fn batch(vectors: Vec<Vector>) -> Self {
188        Self { vectors }
189    }
190}
191
192/// Upsert response
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct UpsertResponse {
195    /// Number of vectors upserted
196    pub upserted_count: u64,
197}
198
199/// Column-based upsert request (Turbopuffer-inspired)
200///
201/// This format is more efficient for bulk upserts as it avoids repeating
202/// field names for each vector. All arrays must have equal length.
203///
204/// # Example
205///
206/// ```rust
207/// use dakera_client::ColumnUpsertRequest;
208/// use std::collections::HashMap;
209///
210/// let request = ColumnUpsertRequest::new(
211///     vec!["id1".to_string(), "id2".to_string()],
212///     vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]],
213/// );
214/// ```
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ColumnUpsertRequest {
217    /// Array of vector IDs (required)
218    pub ids: Vec<String>,
219    /// Array of vectors (required for vector namespaces)
220    pub vectors: Vec<Vec<f32>>,
221    /// Additional attributes as columns (optional)
222    /// Each key is an attribute name, value is array of attribute values
223    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
224    pub attributes: HashMap<String, Vec<serde_json::Value>>,
225    /// TTL in seconds for all vectors (optional)
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub ttl_seconds: Option<u64>,
228    /// Expected dimension (optional, for validation)
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub dimension: Option<usize>,
231}
232
233impl ColumnUpsertRequest {
234    /// Create a new column upsert request
235    pub fn new(ids: Vec<String>, vectors: Vec<Vec<f32>>) -> Self {
236        Self {
237            ids,
238            vectors,
239            attributes: HashMap::new(),
240            ttl_seconds: None,
241            dimension: None,
242        }
243    }
244
245    /// Add an attribute column
246    pub fn with_attribute(
247        mut self,
248        name: impl Into<String>,
249        values: Vec<serde_json::Value>,
250    ) -> Self {
251        self.attributes.insert(name.into(), values);
252        self
253    }
254
255    /// Set TTL for all vectors
256    pub fn with_ttl(mut self, seconds: u64) -> Self {
257        self.ttl_seconds = Some(seconds);
258        self
259    }
260
261    /// Set expected dimension for validation
262    pub fn with_dimension(mut self, dim: usize) -> Self {
263        self.dimension = Some(dim);
264        self
265    }
266
267    /// Get the number of vectors in this request
268    pub fn len(&self) -> usize {
269        self.ids.len()
270    }
271
272    /// Check if the request is empty
273    pub fn is_empty(&self) -> bool {
274        self.ids.is_empty()
275    }
276}
277
278/// Delete vectors request
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct DeleteRequest {
281    /// Vector IDs to delete
282    pub ids: Vec<String>,
283}
284
285impl DeleteRequest {
286    /// Create a delete request for a single ID
287    pub fn single(id: impl Into<String>) -> Self {
288        Self {
289            ids: vec![id.into()],
290        }
291    }
292
293    /// Create a delete request for multiple IDs
294    pub fn batch(ids: Vec<String>) -> Self {
295        Self { ids }
296    }
297}
298
299/// Delete response
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct DeleteResponse {
302    /// Number of vectors deleted
303    pub deleted_count: u64,
304}
305
306// ============================================================================
307// Query Types
308// ============================================================================
309
310/// Read consistency level for queries (Turbopuffer-inspired)
311#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "snake_case")]
313pub enum ReadConsistency {
314    /// Always read from primary/leader node - guarantees latest data
315    Strong,
316    /// Read from any replica - may return slightly stale data but faster
317    #[default]
318    Eventual,
319    /// Read from replicas within staleness bounds
320    #[serde(rename = "bounded_staleness")]
321    BoundedStaleness,
322}
323
324/// Configuration for bounded staleness reads
325#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
326pub struct StalenessConfig {
327    /// Maximum acceptable staleness in milliseconds
328    #[serde(default = "default_max_staleness_ms")]
329    pub max_staleness_ms: u64,
330}
331
332fn default_max_staleness_ms() -> u64 {
333    5000 // 5 seconds default
334}
335
336impl StalenessConfig {
337    /// Create a new staleness config with specified max staleness
338    pub fn new(max_staleness_ms: u64) -> Self {
339        Self { max_staleness_ms }
340    }
341}
342
343/// Distance metric for similarity search
344#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
345#[serde(rename_all = "snake_case")]
346pub enum DistanceMetric {
347    /// Cosine similarity (default)
348    #[default]
349    Cosine,
350    /// Euclidean distance
351    Euclidean,
352    /// Dot product
353    DotProduct,
354}
355
356/// Query request for vector similarity search
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct QueryRequest {
359    /// Query vector
360    pub vector: Vec<f32>,
361    /// Number of results to return
362    pub top_k: u32,
363    /// Distance metric to use
364    #[serde(default)]
365    pub distance_metric: DistanceMetric,
366    /// Optional filter expression
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub filter: Option<serde_json::Value>,
369    /// Whether to include metadata in results
370    #[serde(default = "default_true")]
371    pub include_metadata: bool,
372    /// Whether to include vector values in results
373    #[serde(default)]
374    pub include_vectors: bool,
375    /// Read consistency level
376    #[serde(default)]
377    pub consistency: ReadConsistency,
378    /// Staleness configuration for bounded staleness reads
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub staleness_config: Option<StalenessConfig>,
381}
382
383fn default_true() -> bool {
384    true
385}
386
387impl QueryRequest {
388    /// Create a new query request
389    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
390        Self {
391            vector,
392            top_k,
393            distance_metric: DistanceMetric::default(),
394            filter: None,
395            include_metadata: true,
396            include_vectors: false,
397            consistency: ReadConsistency::default(),
398            staleness_config: None,
399        }
400    }
401
402    /// Add a filter to the query
403    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
404        self.filter = Some(filter);
405        self
406    }
407
408    /// Set whether to include metadata
409    pub fn include_metadata(mut self, include: bool) -> Self {
410        self.include_metadata = include;
411        self
412    }
413
414    /// Set whether to include vector values
415    pub fn include_vectors(mut self, include: bool) -> Self {
416        self.include_vectors = include;
417        self
418    }
419
420    /// Set distance metric
421    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
422        self.distance_metric = metric;
423        self
424    }
425
426    /// Set read consistency level
427    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
428        self.consistency = consistency;
429        self
430    }
431
432    /// Set bounded staleness with max staleness in ms
433    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
434        self.consistency = ReadConsistency::BoundedStaleness;
435        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
436        self
437    }
438
439    /// Use strong consistency (always read from primary)
440    pub fn with_strong_consistency(mut self) -> Self {
441        self.consistency = ReadConsistency::Strong;
442        self
443    }
444}
445
446/// A match result from a query
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct Match {
449    /// Vector ID
450    pub id: String,
451    /// Similarity score
452    pub score: f32,
453    /// Optional metadata
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub metadata: Option<HashMap<String, serde_json::Value>>,
456}
457
458/// Query response
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct QueryResponse {
461    /// Search results
462    #[serde(alias = "matches")]
463    pub results: Vec<Match>,
464}
465
466// ============================================================================
467// Full-Text Search Types
468// ============================================================================
469
470/// A document for full-text indexing
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct Document {
473    /// Document ID
474    pub id: String,
475    /// Document text content
476    pub text: String,
477    /// Optional metadata
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub metadata: Option<HashMap<String, serde_json::Value>>,
480}
481
482impl Document {
483    /// Create a new document
484    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
485        Self {
486            id: id.into(),
487            text: text.into(),
488            metadata: None,
489        }
490    }
491
492    /// Create a new document with metadata
493    pub fn with_metadata(
494        id: impl Into<String>,
495        text: impl Into<String>,
496        metadata: HashMap<String, serde_json::Value>,
497    ) -> Self {
498        Self {
499            id: id.into(),
500            text: text.into(),
501            metadata: Some(metadata),
502        }
503    }
504}
505
506/// Index documents request
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct IndexDocumentsRequest {
509    /// Documents to index
510    pub documents: Vec<Document>,
511}
512
513/// Index documents response
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct IndexDocumentsResponse {
516    /// Number of documents indexed
517    pub indexed_count: u64,
518}
519
520/// Full-text search request
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct FullTextSearchRequest {
523    /// Search query
524    pub query: String,
525    /// Maximum number of results
526    pub top_k: u32,
527    /// Optional filter
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub filter: Option<serde_json::Value>,
530}
531
532impl FullTextSearchRequest {
533    /// Create a new full-text search request
534    pub fn new(query: impl Into<String>, top_k: u32) -> Self {
535        Self {
536            query: query.into(),
537            top_k,
538            filter: None,
539        }
540    }
541
542    /// Add a filter to the search
543    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
544        self.filter = Some(filter);
545        self
546    }
547}
548
549/// Full-text search result
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct FullTextMatch {
552    /// Document ID
553    pub id: String,
554    /// BM25 score
555    pub score: f32,
556    /// Document text
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub text: Option<String>,
559    /// Optional metadata
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub metadata: Option<HashMap<String, serde_json::Value>>,
562}
563
564/// Full-text search response
565#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct FullTextSearchResponse {
567    /// Matched documents
568    #[serde(alias = "matches")]
569    pub results: Vec<FullTextMatch>,
570}
571
572/// Full-text index statistics
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct FullTextStats {
575    /// Number of documents indexed
576    pub document_count: u64,
577    /// Number of unique terms
578    pub term_count: u64,
579}
580
581// ============================================================================
582// Hybrid Search Types
583// ============================================================================
584
585/// Hybrid search request combining vector and full-text search.
586///
587/// When `vector` is `None` the server falls back to BM25-only full-text search.
588/// When provided, results are blended with vector similarity according to `vector_weight`.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct HybridSearchRequest {
591    /// Optional query vector. Omit for BM25-only search.
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub vector: Option<Vec<f32>>,
594    /// Text query
595    pub text: String,
596    /// Number of results to return
597    pub top_k: u32,
598    /// Weight for vector search (0.0-1.0)
599    #[serde(default = "default_vector_weight")]
600    pub vector_weight: f32,
601    /// Optional filter
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub filter: Option<serde_json::Value>,
604}
605
606fn default_vector_weight() -> f32 {
607    0.5
608}
609
610impl HybridSearchRequest {
611    /// Create a new hybrid search request with a query vector (hybrid mode).
612    pub fn new(vector: Vec<f32>, text: impl Into<String>, top_k: u32) -> Self {
613        Self {
614            vector: Some(vector),
615            text: text.into(),
616            top_k,
617            vector_weight: 0.5,
618            filter: None,
619        }
620    }
621
622    /// Create a BM25-only full-text search request (no vector required).
623    pub fn text_only(text: impl Into<String>, top_k: u32) -> Self {
624        Self {
625            vector: None,
626            text: text.into(),
627            top_k,
628            vector_weight: 0.5,
629            filter: None,
630        }
631    }
632
633    /// Set the vector weight (text weight is 1.0 - vector_weight)
634    pub fn with_vector_weight(mut self, weight: f32) -> Self {
635        self.vector_weight = weight.clamp(0.0, 1.0);
636        self
637    }
638
639    /// Add a filter to the search
640    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
641        self.filter = Some(filter);
642        self
643    }
644}
645
646/// Hybrid search response
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct HybridSearchResponse {
649    /// Matched results
650    #[serde(alias = "matches")]
651    pub results: Vec<Match>,
652}
653
654// ============================================================================
655// Operations Types
656// ============================================================================
657
658/// System diagnostics
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct SystemDiagnostics {
661    /// System information
662    pub system: SystemInfo,
663    /// Resource usage
664    pub resources: ResourceUsage,
665    /// Component health
666    pub components: ComponentHealth,
667    /// Number of active jobs
668    pub active_jobs: u64,
669}
670
671/// System information
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct SystemInfo {
674    /// Dakera version
675    pub version: String,
676    /// Rust version
677    pub rust_version: String,
678    /// Uptime in seconds
679    pub uptime_seconds: u64,
680    /// Process ID
681    pub pid: u32,
682}
683
684/// Resource usage metrics
685#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct ResourceUsage {
687    /// Memory usage in bytes
688    pub memory_bytes: u64,
689    /// Thread count
690    pub thread_count: u64,
691    /// Open file descriptors
692    pub open_fds: u64,
693    /// CPU usage percentage
694    pub cpu_percent: Option<f64>,
695}
696
697/// Component health status
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct ComponentHealth {
700    /// Storage health
701    pub storage: HealthStatus,
702    /// Search engine health
703    pub search_engine: HealthStatus,
704    /// Cache health
705    pub cache: HealthStatus,
706    /// gRPC health
707    pub grpc: HealthStatus,
708}
709
710/// Health status for a component
711#[derive(Debug, Clone, Serialize, Deserialize)]
712pub struct HealthStatus {
713    /// Is the component healthy
714    pub healthy: bool,
715    /// Status message
716    pub message: String,
717    /// Last check timestamp
718    pub last_check: u64,
719}
720
721/// Background job information
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct JobInfo {
724    /// Job ID
725    pub id: String,
726    /// Job type
727    pub job_type: String,
728    /// Current status
729    pub status: String,
730    /// Creation timestamp
731    pub created_at: u64,
732    /// Start timestamp
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub started_at: Option<u64>,
735    /// Completion timestamp
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub completed_at: Option<u64>,
738    /// Progress percentage
739    pub progress: u8,
740    /// Status message
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub message: Option<String>,
743    /// Job metadata
744    #[serde(default)]
745    pub metadata: std::collections::HashMap<String, String>,
746}
747
748/// Compaction request
749#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct CompactionRequest {
751    /// Namespace to compact (None = all)
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub namespace: Option<String>,
754    /// Force compaction
755    #[serde(default)]
756    pub force: bool,
757}
758
759/// Compaction response
760#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct CompactionResponse {
762    /// Job ID for tracking
763    pub job_id: String,
764    /// Status message
765    pub message: String,
766}
767
768// ============================================================================
769// Cache Warming Types (Turbopuffer-inspired)
770// ============================================================================
771
772/// Priority level for cache warming operations
773#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
774#[serde(rename_all = "snake_case")]
775pub enum WarmingPriority {
776    /// Highest priority - warm immediately, preempt other operations
777    Critical,
778    /// High priority - warm soon
779    High,
780    /// Normal priority (default)
781    #[default]
782    Normal,
783    /// Low priority - warm when resources available
784    Low,
785    /// Background priority - warm during idle time only
786    Background,
787}
788
789/// Target cache tier for warming
790#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
791#[serde(rename_all = "snake_case")]
792pub enum WarmingTargetTier {
793    /// L1 in-memory cache (Moka) - fastest, limited size
794    L1,
795    /// L2 local disk cache (RocksDB) - larger, persistent
796    #[default]
797    L2,
798    /// Both L1 and L2 caches
799    Both,
800}
801
802/// Access pattern hint for cache optimization
803#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
804#[serde(rename_all = "snake_case")]
805pub enum AccessPatternHint {
806    /// Random access pattern
807    #[default]
808    Random,
809    /// Sequential access pattern
810    Sequential,
811    /// Temporal locality (recently accessed items accessed again)
812    Temporal,
813    /// Spatial locality (nearby items accessed together)
814    Spatial,
815}
816
817/// Cache warming request with priority hints
818#[derive(Debug, Clone, Serialize, Deserialize)]
819pub struct WarmCacheRequest {
820    /// Namespace to warm
821    pub namespace: String,
822    /// Specific vector IDs to warm (None = all)
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub vector_ids: Option<Vec<String>>,
825    /// Warming priority level
826    #[serde(default)]
827    pub priority: WarmingPriority,
828    /// Target cache tier
829    #[serde(default)]
830    pub target_tier: WarmingTargetTier,
831    /// Run warming in background (non-blocking)
832    #[serde(default)]
833    pub background: bool,
834    /// TTL hint in seconds
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub ttl_hint_seconds: Option<u64>,
837    /// Access pattern hint for optimization
838    #[serde(default)]
839    pub access_pattern: AccessPatternHint,
840    /// Maximum vectors to warm
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub max_vectors: Option<usize>,
843}
844
845impl WarmCacheRequest {
846    /// Create a new cache warming request for a namespace
847    pub fn new(namespace: impl Into<String>) -> Self {
848        Self {
849            namespace: namespace.into(),
850            vector_ids: None,
851            priority: WarmingPriority::default(),
852            target_tier: WarmingTargetTier::default(),
853            background: false,
854            ttl_hint_seconds: None,
855            access_pattern: AccessPatternHint::default(),
856            max_vectors: None,
857        }
858    }
859
860    /// Warm specific vector IDs
861    pub fn with_vector_ids(mut self, ids: Vec<String>) -> Self {
862        self.vector_ids = Some(ids);
863        self
864    }
865
866    /// Set warming priority
867    pub fn with_priority(mut self, priority: WarmingPriority) -> Self {
868        self.priority = priority;
869        self
870    }
871
872    /// Set target cache tier
873    pub fn with_target_tier(mut self, tier: WarmingTargetTier) -> Self {
874        self.target_tier = tier;
875        self
876    }
877
878    /// Run warming in background
879    pub fn in_background(mut self) -> Self {
880        self.background = true;
881        self
882    }
883
884    /// Set TTL hint
885    pub fn with_ttl(mut self, seconds: u64) -> Self {
886        self.ttl_hint_seconds = Some(seconds);
887        self
888    }
889
890    /// Set access pattern hint
891    pub fn with_access_pattern(mut self, pattern: AccessPatternHint) -> Self {
892        self.access_pattern = pattern;
893        self
894    }
895
896    /// Limit number of vectors to warm
897    pub fn with_max_vectors(mut self, max: usize) -> Self {
898        self.max_vectors = Some(max);
899        self
900    }
901}
902
903/// Cache warming response
904#[derive(Debug, Clone, Serialize, Deserialize)]
905pub struct WarmCacheResponse {
906    /// Operation success
907    pub success: bool,
908    /// Number of entries warmed
909    pub entries_warmed: u64,
910    /// Number of entries already warm (skipped)
911    pub entries_skipped: u64,
912    /// Job ID for tracking background operations
913    #[serde(skip_serializing_if = "Option::is_none")]
914    pub job_id: Option<String>,
915    /// Status message
916    pub message: String,
917    /// Estimated completion time for background jobs (ISO 8601)
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub estimated_completion: Option<String>,
920    /// Target tier that was warmed
921    pub target_tier: WarmingTargetTier,
922    /// Priority that was used
923    pub priority: WarmingPriority,
924    /// Bytes warmed (approximate)
925    #[serde(skip_serializing_if = "Option::is_none")]
926    pub bytes_warmed: Option<u64>,
927}
928
929// ============================================================================
930// Export Types (Turbopuffer-inspired)
931// ============================================================================
932
933/// Request to export vectors from a namespace with pagination
934#[derive(Debug, Clone, Serialize, Deserialize)]
935pub struct ExportRequest {
936    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
937    #[serde(default = "default_export_top_k")]
938    pub top_k: usize,
939    /// Cursor for pagination - the last vector ID from previous page
940    #[serde(skip_serializing_if = "Option::is_none")]
941    pub cursor: Option<String>,
942    /// Whether to include vector values in the response (default: true)
943    #[serde(default = "default_true")]
944    pub include_vectors: bool,
945    /// Whether to include metadata in the response (default: true)
946    #[serde(default = "default_true")]
947    pub include_metadata: bool,
948}
949
950fn default_export_top_k() -> usize {
951    1000
952}
953
954impl Default for ExportRequest {
955    fn default() -> Self {
956        Self {
957            top_k: 1000,
958            cursor: None,
959            include_vectors: true,
960            include_metadata: true,
961        }
962    }
963}
964
965impl ExportRequest {
966    /// Create a new export request with default settings
967    pub fn new() -> Self {
968        Self::default()
969    }
970
971    /// Set the maximum number of vectors to return per page
972    pub fn with_top_k(mut self, top_k: usize) -> Self {
973        self.top_k = top_k;
974        self
975    }
976
977    /// Set the pagination cursor
978    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
979        self.cursor = Some(cursor.into());
980        self
981    }
982
983    /// Set whether to include vector values
984    pub fn include_vectors(mut self, include: bool) -> Self {
985        self.include_vectors = include;
986        self
987    }
988
989    /// Set whether to include metadata
990    pub fn include_metadata(mut self, include: bool) -> Self {
991        self.include_metadata = include;
992        self
993    }
994}
995
996/// A single exported vector record
997#[derive(Debug, Clone, Serialize, Deserialize)]
998pub struct ExportedVector {
999    /// Vector ID
1000    pub id: String,
1001    /// Vector values (optional based on include_vectors)
1002    #[serde(skip_serializing_if = "Option::is_none")]
1003    pub values: Option<Vec<f32>>,
1004    /// Metadata (optional based on include_metadata)
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub metadata: Option<serde_json::Value>,
1007    /// TTL in seconds if set
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub ttl_seconds: Option<u64>,
1010}
1011
1012/// Response from export operation
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1014pub struct ExportResponse {
1015    /// Exported vectors for this page
1016    pub vectors: Vec<ExportedVector>,
1017    /// Cursor for next page (None if this is the last page)
1018    #[serde(skip_serializing_if = "Option::is_none")]
1019    pub next_cursor: Option<String>,
1020    /// Total vectors in namespace (for progress tracking)
1021    pub total_count: usize,
1022    /// Number of vectors returned in this page
1023    pub returned_count: usize,
1024}
1025
1026// ============================================================================
1027// Batch Query Types
1028// ============================================================================
1029
1030/// A single query within a batch request
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032pub struct BatchQueryItem {
1033    /// Unique identifier for this query within the batch
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    pub id: Option<String>,
1036    /// The query vector
1037    pub vector: Vec<f32>,
1038    /// Number of results to return
1039    #[serde(default = "default_batch_top_k")]
1040    pub top_k: u32,
1041    /// Optional filter expression
1042    #[serde(skip_serializing_if = "Option::is_none")]
1043    pub filter: Option<serde_json::Value>,
1044    /// Whether to include metadata in results
1045    #[serde(default)]
1046    pub include_metadata: bool,
1047    /// Read consistency level
1048    #[serde(default)]
1049    pub consistency: ReadConsistency,
1050    /// Staleness configuration for bounded staleness reads
1051    #[serde(skip_serializing_if = "Option::is_none")]
1052    pub staleness_config: Option<StalenessConfig>,
1053}
1054
1055fn default_batch_top_k() -> u32 {
1056    10
1057}
1058
1059impl BatchQueryItem {
1060    /// Create a new batch query item
1061    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
1062        Self {
1063            id: None,
1064            vector,
1065            top_k,
1066            filter: None,
1067            include_metadata: true,
1068            consistency: ReadConsistency::default(),
1069            staleness_config: None,
1070        }
1071    }
1072
1073    /// Set a unique identifier for this query
1074    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1075        self.id = Some(id.into());
1076        self
1077    }
1078
1079    /// Add a filter to the query
1080    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1081        self.filter = Some(filter);
1082        self
1083    }
1084
1085    /// Set whether to include metadata
1086    pub fn include_metadata(mut self, include: bool) -> Self {
1087        self.include_metadata = include;
1088        self
1089    }
1090
1091    /// Set read consistency level
1092    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1093        self.consistency = consistency;
1094        self
1095    }
1096
1097    /// Set bounded staleness with max staleness in ms
1098    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
1099        self.consistency = ReadConsistency::BoundedStaleness;
1100        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
1101        self
1102    }
1103}
1104
1105/// Batch query request - execute multiple queries in parallel
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct BatchQueryRequest {
1108    /// List of queries to execute
1109    pub queries: Vec<BatchQueryItem>,
1110}
1111
1112impl BatchQueryRequest {
1113    /// Create a new batch query request
1114    pub fn new(queries: Vec<BatchQueryItem>) -> Self {
1115        Self { queries }
1116    }
1117
1118    /// Create a batch query request from a single query
1119    pub fn single(query: BatchQueryItem) -> Self {
1120        Self {
1121            queries: vec![query],
1122        }
1123    }
1124}
1125
1126/// Results for a single query within a batch
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1128pub struct BatchQueryResult {
1129    /// The query identifier (if provided in request)
1130    #[serde(skip_serializing_if = "Option::is_none")]
1131    pub id: Option<String>,
1132    /// Query results (empty if an error occurred)
1133    pub results: Vec<Match>,
1134    /// Query execution time in milliseconds
1135    pub latency_ms: f64,
1136    /// Error message if this individual query failed
1137    #[serde(skip_serializing_if = "Option::is_none")]
1138    pub error: Option<String>,
1139}
1140
1141/// Batch query response
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1143pub struct BatchQueryResponse {
1144    /// Results for each query in the batch
1145    pub results: Vec<BatchQueryResult>,
1146    /// Total execution time in milliseconds
1147    pub total_latency_ms: f64,
1148    /// Number of queries executed
1149    pub query_count: usize,
1150}
1151
1152// ============================================================================
1153// Multi-Vector Search Types
1154// ============================================================================
1155
1156/// Request for multi-vector search with positive and negative vectors
1157#[derive(Debug, Clone, Serialize, Deserialize)]
1158pub struct MultiVectorSearchRequest {
1159    /// Positive vectors to search towards (required, at least one)
1160    pub positive_vectors: Vec<Vec<f32>>,
1161    /// Weights for positive vectors (optional, defaults to equal weights)
1162    #[serde(skip_serializing_if = "Option::is_none")]
1163    pub positive_weights: Option<Vec<f32>>,
1164    /// Negative vectors to search away from (optional)
1165    #[serde(skip_serializing_if = "Option::is_none")]
1166    pub negative_vectors: Option<Vec<Vec<f32>>>,
1167    /// Weights for negative vectors (optional, defaults to equal weights)
1168    #[serde(skip_serializing_if = "Option::is_none")]
1169    pub negative_weights: Option<Vec<f32>>,
1170    /// Number of results to return
1171    #[serde(default = "default_multi_vector_top_k")]
1172    pub top_k: u32,
1173    /// Distance metric to use
1174    #[serde(default)]
1175    pub distance_metric: DistanceMetric,
1176    /// Minimum score threshold
1177    #[serde(skip_serializing_if = "Option::is_none")]
1178    pub score_threshold: Option<f32>,
1179    /// Enable MMR (Maximal Marginal Relevance) for diversity
1180    #[serde(default)]
1181    pub enable_mmr: bool,
1182    /// Lambda parameter for MMR (0 = max diversity, 1 = max relevance)
1183    #[serde(default = "default_mmr_lambda")]
1184    pub mmr_lambda: f32,
1185    /// Include metadata in results
1186    #[serde(default = "default_true")]
1187    pub include_metadata: bool,
1188    /// Include vectors in results
1189    #[serde(default)]
1190    pub include_vectors: bool,
1191    /// Optional metadata filter
1192    #[serde(skip_serializing_if = "Option::is_none")]
1193    pub filter: Option<serde_json::Value>,
1194    /// Read consistency level
1195    #[serde(default)]
1196    pub consistency: ReadConsistency,
1197    /// Staleness configuration for bounded staleness reads
1198    #[serde(skip_serializing_if = "Option::is_none")]
1199    pub staleness_config: Option<StalenessConfig>,
1200}
1201
1202fn default_multi_vector_top_k() -> u32 {
1203    10
1204}
1205
1206fn default_mmr_lambda() -> f32 {
1207    0.5
1208}
1209
1210impl MultiVectorSearchRequest {
1211    /// Create a new multi-vector search request with positive vectors
1212    pub fn new(positive_vectors: Vec<Vec<f32>>) -> Self {
1213        Self {
1214            positive_vectors,
1215            positive_weights: None,
1216            negative_vectors: None,
1217            negative_weights: None,
1218            top_k: 10,
1219            distance_metric: DistanceMetric::default(),
1220            score_threshold: None,
1221            enable_mmr: false,
1222            mmr_lambda: 0.5,
1223            include_metadata: true,
1224            include_vectors: false,
1225            filter: None,
1226            consistency: ReadConsistency::default(),
1227            staleness_config: None,
1228        }
1229    }
1230
1231    /// Set the number of results to return
1232    pub fn with_top_k(mut self, top_k: u32) -> Self {
1233        self.top_k = top_k;
1234        self
1235    }
1236
1237    /// Add weights for positive vectors
1238    pub fn with_positive_weights(mut self, weights: Vec<f32>) -> Self {
1239        self.positive_weights = Some(weights);
1240        self
1241    }
1242
1243    /// Add negative vectors to search away from
1244    pub fn with_negative_vectors(mut self, vectors: Vec<Vec<f32>>) -> Self {
1245        self.negative_vectors = Some(vectors);
1246        self
1247    }
1248
1249    /// Add weights for negative vectors
1250    pub fn with_negative_weights(mut self, weights: Vec<f32>) -> Self {
1251        self.negative_weights = Some(weights);
1252        self
1253    }
1254
1255    /// Set distance metric
1256    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1257        self.distance_metric = metric;
1258        self
1259    }
1260
1261    /// Set minimum score threshold
1262    pub fn with_score_threshold(mut self, threshold: f32) -> Self {
1263        self.score_threshold = Some(threshold);
1264        self
1265    }
1266
1267    /// Enable MMR for diversity
1268    pub fn with_mmr(mut self, lambda: f32) -> Self {
1269        self.enable_mmr = true;
1270        self.mmr_lambda = lambda.clamp(0.0, 1.0);
1271        self
1272    }
1273
1274    /// Set whether to include metadata
1275    pub fn include_metadata(mut self, include: bool) -> Self {
1276        self.include_metadata = include;
1277        self
1278    }
1279
1280    /// Set whether to include vectors
1281    pub fn include_vectors(mut self, include: bool) -> Self {
1282        self.include_vectors = include;
1283        self
1284    }
1285
1286    /// Add a filter
1287    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1288        self.filter = Some(filter);
1289        self
1290    }
1291
1292    /// Set read consistency level
1293    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1294        self.consistency = consistency;
1295        self
1296    }
1297}
1298
1299/// Single result from multi-vector search
1300#[derive(Debug, Clone, Serialize, Deserialize)]
1301pub struct MultiVectorSearchResult {
1302    /// Vector ID
1303    pub id: String,
1304    /// Similarity score
1305    pub score: f32,
1306    /// MMR score (if MMR enabled)
1307    #[serde(skip_serializing_if = "Option::is_none")]
1308    pub mmr_score: Option<f32>,
1309    /// Original rank before reranking
1310    #[serde(skip_serializing_if = "Option::is_none")]
1311    pub original_rank: Option<usize>,
1312    /// Optional metadata
1313    #[serde(skip_serializing_if = "Option::is_none")]
1314    pub metadata: Option<HashMap<String, serde_json::Value>>,
1315    /// Optional vector values
1316    #[serde(skip_serializing_if = "Option::is_none")]
1317    pub vector: Option<Vec<f32>>,
1318}
1319
1320/// Response from multi-vector search
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322pub struct MultiVectorSearchResponse {
1323    /// Search results
1324    pub results: Vec<MultiVectorSearchResult>,
1325    /// The computed query vector (weighted combination of positive - negative)
1326    #[serde(skip_serializing_if = "Option::is_none")]
1327    pub computed_query_vector: Option<Vec<f32>>,
1328}
1329
1330// ============================================================================
1331// Aggregation Types (Turbopuffer-inspired)
1332// ============================================================================
1333
1334/// Aggregate function for computing values across documents
1335#[derive(Debug, Clone, Serialize, Deserialize)]
1336#[serde(untagged)]
1337pub enum AggregateFunction {
1338    /// Count matching documents
1339    Count,
1340    /// Sum numeric attribute values
1341    Sum { field: String },
1342    /// Average numeric attribute values
1343    Avg { field: String },
1344    /// Minimum numeric attribute value
1345    Min { field: String },
1346    /// Maximum numeric attribute value
1347    Max { field: String },
1348}
1349
1350/// Request for aggregation query (Turbopuffer-inspired)
1351#[derive(Debug, Clone, Serialize, Deserialize)]
1352pub struct AggregationRequest {
1353    /// Named aggregations to compute
1354    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
1355    pub aggregate_by: HashMap<String, serde_json::Value>,
1356    /// Fields to group results by (optional)
1357    /// Example: ["category", "status"]
1358    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1359    pub group_by: Vec<String>,
1360    /// Filter to apply before aggregation
1361    #[serde(skip_serializing_if = "Option::is_none")]
1362    pub filter: Option<serde_json::Value>,
1363    /// Maximum number of groups to return (default: 100)
1364    #[serde(default = "default_agg_limit")]
1365    pub limit: usize,
1366}
1367
1368fn default_agg_limit() -> usize {
1369    100
1370}
1371
1372impl AggregationRequest {
1373    /// Create a new aggregation request with a single aggregation
1374    pub fn new() -> Self {
1375        Self {
1376            aggregate_by: HashMap::new(),
1377            group_by: Vec::new(),
1378            filter: None,
1379            limit: 100,
1380        }
1381    }
1382
1383    /// Add a count aggregation
1384    pub fn with_count(mut self, name: impl Into<String>) -> Self {
1385        self.aggregate_by
1386            .insert(name.into(), serde_json::json!(["Count"]));
1387        self
1388    }
1389
1390    /// Add a sum aggregation
1391    pub fn with_sum(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1392        self.aggregate_by
1393            .insert(name.into(), serde_json::json!(["Sum", field.into()]));
1394        self
1395    }
1396
1397    /// Add an average aggregation
1398    pub fn with_avg(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1399        self.aggregate_by
1400            .insert(name.into(), serde_json::json!(["Avg", field.into()]));
1401        self
1402    }
1403
1404    /// Add a min aggregation
1405    pub fn with_min(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1406        self.aggregate_by
1407            .insert(name.into(), serde_json::json!(["Min", field.into()]));
1408        self
1409    }
1410
1411    /// Add a max aggregation
1412    pub fn with_max(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1413        self.aggregate_by
1414            .insert(name.into(), serde_json::json!(["Max", field.into()]));
1415        self
1416    }
1417
1418    /// Set group by fields
1419    pub fn group_by(mut self, fields: Vec<String>) -> Self {
1420        self.group_by = fields;
1421        self
1422    }
1423
1424    /// Add a single group by field
1425    pub fn with_group_by(mut self, field: impl Into<String>) -> Self {
1426        self.group_by.push(field.into());
1427        self
1428    }
1429
1430    /// Set filter for aggregation
1431    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1432        self.filter = Some(filter);
1433        self
1434    }
1435
1436    /// Set maximum number of groups to return
1437    pub fn with_limit(mut self, limit: usize) -> Self {
1438        self.limit = limit;
1439        self
1440    }
1441}
1442
1443impl Default for AggregationRequest {
1444    fn default() -> Self {
1445        Self::new()
1446    }
1447}
1448
1449/// Response for aggregation query
1450#[derive(Debug, Clone, Serialize, Deserialize)]
1451pub struct AggregationResponse {
1452    /// Aggregation results (without grouping)
1453    #[serde(skip_serializing_if = "Option::is_none")]
1454    pub aggregations: Option<HashMap<String, serde_json::Value>>,
1455    /// Grouped aggregation results (with group_by)
1456    #[serde(skip_serializing_if = "Option::is_none")]
1457    pub aggregation_groups: Option<Vec<AggregationGroup>>,
1458}
1459
1460/// Single group in aggregation results
1461#[derive(Debug, Clone, Serialize, Deserialize)]
1462pub struct AggregationGroup {
1463    /// Group key values (flattened into object)
1464    #[serde(flatten)]
1465    pub group_key: HashMap<String, serde_json::Value>,
1466    /// Aggregation results for this group
1467    #[serde(flatten)]
1468    pub aggregations: HashMap<String, serde_json::Value>,
1469}
1470
1471// ============================================================================
1472// Unified Query Types (Turbopuffer-inspired)
1473// ============================================================================
1474
1475/// Vector search method for unified query
1476#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1477pub enum VectorSearchMethod {
1478    /// Approximate Nearest Neighbor (fast, default)
1479    #[default]
1480    ANN,
1481    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
1482    #[serde(rename = "kNN")]
1483    KNN,
1484}
1485
1486/// Sort direction for attribute ordering
1487#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1488#[serde(rename_all = "lowercase")]
1489pub enum SortDirection {
1490    /// Ascending order
1491    Asc,
1492    /// Descending order
1493    #[default]
1494    Desc,
1495}
1496
1497/// Ranking function for unified query API
1498/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
1499#[derive(Debug, Clone, Serialize, Deserialize)]
1500#[serde(untagged)]
1501pub enum RankBy {
1502    /// Vector search: uses field, method, and query_vector
1503    VectorSearch {
1504        field: String,
1505        method: VectorSearchMethod,
1506        query_vector: Vec<f32>,
1507    },
1508    /// Full-text BM25 search
1509    FullTextSearch {
1510        field: String,
1511        method: String, // Always "BM25"
1512        query: String,
1513    },
1514    /// Attribute ordering
1515    AttributeOrder {
1516        field: String,
1517        direction: SortDirection,
1518    },
1519    /// Sum of multiple ranking functions
1520    Sum(Vec<RankBy>),
1521    /// Max of multiple ranking functions
1522    Max(Vec<RankBy>),
1523    /// Product with weight
1524    Product { weight: f32, ranking: Box<RankBy> },
1525}
1526
1527impl RankBy {
1528    /// Create a vector search ranking using ANN
1529    pub fn vector_ann(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1530        RankBy::VectorSearch {
1531            field: field.into(),
1532            method: VectorSearchMethod::ANN,
1533            query_vector,
1534        }
1535    }
1536
1537    /// Create a vector search ranking using ANN on the default "vector" field
1538    pub fn ann(query_vector: Vec<f32>) -> Self {
1539        Self::vector_ann("vector", query_vector)
1540    }
1541
1542    /// Create a vector search ranking using exact kNN
1543    pub fn vector_knn(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1544        RankBy::VectorSearch {
1545            field: field.into(),
1546            method: VectorSearchMethod::KNN,
1547            query_vector,
1548        }
1549    }
1550
1551    /// Create a vector search ranking using kNN on the default "vector" field
1552    pub fn knn(query_vector: Vec<f32>) -> Self {
1553        Self::vector_knn("vector", query_vector)
1554    }
1555
1556    /// Create a BM25 full-text search ranking
1557    pub fn bm25(field: impl Into<String>, query: impl Into<String>) -> Self {
1558        RankBy::FullTextSearch {
1559            field: field.into(),
1560            method: "BM25".to_string(),
1561            query: query.into(),
1562        }
1563    }
1564
1565    /// Create an attribute ordering ranking (ascending)
1566    pub fn asc(field: impl Into<String>) -> Self {
1567        RankBy::AttributeOrder {
1568            field: field.into(),
1569            direction: SortDirection::Asc,
1570        }
1571    }
1572
1573    /// Create an attribute ordering ranking (descending)
1574    pub fn desc(field: impl Into<String>) -> Self {
1575        RankBy::AttributeOrder {
1576            field: field.into(),
1577            direction: SortDirection::Desc,
1578        }
1579    }
1580
1581    /// Sum multiple ranking functions together
1582    pub fn sum(rankings: Vec<RankBy>) -> Self {
1583        RankBy::Sum(rankings)
1584    }
1585
1586    /// Take the max of multiple ranking functions
1587    pub fn max(rankings: Vec<RankBy>) -> Self {
1588        RankBy::Max(rankings)
1589    }
1590
1591    /// Apply a weight multiplier to a ranking function
1592    pub fn product(weight: f32, ranking: RankBy) -> Self {
1593        RankBy::Product {
1594            weight,
1595            ranking: Box::new(ranking),
1596        }
1597    }
1598}
1599
1600/// Unified query request with flexible ranking options (Turbopuffer-inspired)
1601///
1602/// # Example
1603///
1604/// ```rust
1605/// use dakera_client::UnifiedQueryRequest;
1606///
1607/// // Vector ANN search
1608/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10);
1609///
1610/// // Full-text BM25 search
1611/// let request = UnifiedQueryRequest::fulltext_search("content", "hello world", 10);
1612///
1613/// // Custom rank_by with filters
1614/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10)
1615///     .with_filter(serde_json::json!({"category": {"$eq": "science"}}));
1616/// ```
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1618pub struct UnifiedQueryRequest {
1619    /// How to rank documents (required)
1620    pub rank_by: serde_json::Value,
1621    /// Number of results to return
1622    #[serde(default = "default_unified_top_k")]
1623    pub top_k: usize,
1624    /// Optional metadata filter
1625    #[serde(skip_serializing_if = "Option::is_none")]
1626    pub filter: Option<serde_json::Value>,
1627    /// Include metadata in results
1628    #[serde(default = "default_true")]
1629    pub include_metadata: bool,
1630    /// Include vectors in results
1631    #[serde(default)]
1632    pub include_vectors: bool,
1633    /// Distance metric for vector search (default: cosine)
1634    #[serde(default)]
1635    pub distance_metric: DistanceMetric,
1636}
1637
1638fn default_unified_top_k() -> usize {
1639    10
1640}
1641
1642impl UnifiedQueryRequest {
1643    /// Create a new unified query request with vector ANN search
1644    pub fn vector_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1645        Self {
1646            rank_by: serde_json::json!(["ANN", query_vector]),
1647            top_k,
1648            filter: None,
1649            include_metadata: true,
1650            include_vectors: false,
1651            distance_metric: DistanceMetric::default(),
1652        }
1653    }
1654
1655    /// Create a new unified query request with vector kNN search
1656    pub fn vector_knn_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1657        Self {
1658            rank_by: serde_json::json!(["kNN", query_vector]),
1659            top_k,
1660            filter: None,
1661            include_metadata: true,
1662            include_vectors: false,
1663            distance_metric: DistanceMetric::default(),
1664        }
1665    }
1666
1667    /// Create a new unified query request with full-text BM25 search
1668    pub fn fulltext_search(
1669        field: impl Into<String>,
1670        query: impl Into<String>,
1671        top_k: usize,
1672    ) -> Self {
1673        Self {
1674            rank_by: serde_json::json!([field.into(), "BM25", query.into()]),
1675            top_k,
1676            filter: None,
1677            include_metadata: true,
1678            include_vectors: false,
1679            distance_metric: DistanceMetric::default(),
1680        }
1681    }
1682
1683    /// Create a new unified query request with attribute ordering
1684    pub fn attribute_order(
1685        field: impl Into<String>,
1686        direction: SortDirection,
1687        top_k: usize,
1688    ) -> Self {
1689        let dir = match direction {
1690            SortDirection::Asc => "asc",
1691            SortDirection::Desc => "desc",
1692        };
1693        Self {
1694            rank_by: serde_json::json!([field.into(), dir]),
1695            top_k,
1696            filter: None,
1697            include_metadata: true,
1698            include_vectors: false,
1699            distance_metric: DistanceMetric::default(),
1700        }
1701    }
1702
1703    /// Create a unified query with a raw rank_by JSON value
1704    pub fn with_rank_by(rank_by: serde_json::Value, top_k: usize) -> Self {
1705        Self {
1706            rank_by,
1707            top_k,
1708            filter: None,
1709            include_metadata: true,
1710            include_vectors: false,
1711            distance_metric: DistanceMetric::default(),
1712        }
1713    }
1714
1715    /// Add a filter to the query
1716    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1717        self.filter = Some(filter);
1718        self
1719    }
1720
1721    /// Set whether to include metadata
1722    pub fn include_metadata(mut self, include: bool) -> Self {
1723        self.include_metadata = include;
1724        self
1725    }
1726
1727    /// Set whether to include vector values
1728    pub fn include_vectors(mut self, include: bool) -> Self {
1729        self.include_vectors = include;
1730        self
1731    }
1732
1733    /// Set the distance metric
1734    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1735        self.distance_metric = metric;
1736        self
1737    }
1738
1739    /// Set the number of results to return
1740    pub fn with_top_k(mut self, top_k: usize) -> Self {
1741        self.top_k = top_k;
1742        self
1743    }
1744}
1745
1746/// Single result from unified query
1747#[derive(Debug, Clone, Serialize, Deserialize)]
1748pub struct UnifiedSearchResult {
1749    /// Vector/document ID
1750    pub id: String,
1751    /// Ranking score (distance for vector search, BM25 score for text)
1752    /// Named $dist for Turbopuffer compatibility
1753    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
1754    pub dist: Option<f32>,
1755    /// Metadata if requested
1756    #[serde(skip_serializing_if = "Option::is_none")]
1757    pub metadata: Option<serde_json::Value>,
1758    /// Vector values if requested
1759    #[serde(skip_serializing_if = "Option::is_none")]
1760    pub vector: Option<Vec<f32>>,
1761}
1762
1763/// Unified query response
1764#[derive(Debug, Clone, Serialize, Deserialize)]
1765pub struct UnifiedQueryResponse {
1766    /// Search results ordered by rank_by score
1767    pub results: Vec<UnifiedSearchResult>,
1768    /// Cursor for pagination (if more results available)
1769    #[serde(skip_serializing_if = "Option::is_none")]
1770    pub next_cursor: Option<String>,
1771}
1772
1773// ============================================================================
1774// Query Explain Types
1775// ============================================================================
1776
1777fn default_explain_top_k() -> usize {
1778    10
1779}
1780
1781/// Query type for explain
1782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1783#[serde(rename_all = "snake_case")]
1784#[derive(Default)]
1785pub enum ExplainQueryType {
1786    /// Vector similarity search
1787    #[default]
1788    VectorSearch,
1789    /// Full-text search
1790    FullTextSearch,
1791    /// Hybrid search combining vector and text
1792    HybridSearch,
1793    /// Multi-vector search with positive/negative vectors
1794    MultiVector,
1795    /// Batch query execution
1796    BatchQuery,
1797}
1798
1799/// Query explain request
1800#[derive(Debug, Clone, Serialize, Deserialize)]
1801pub struct QueryExplainRequest {
1802    /// Type of query to explain
1803    #[serde(default)]
1804    pub query_type: ExplainQueryType,
1805    /// Query vector (for vector searches)
1806    #[serde(skip_serializing_if = "Option::is_none")]
1807    pub vector: Option<Vec<f32>>,
1808    /// Number of results to return
1809    #[serde(default = "default_explain_top_k")]
1810    pub top_k: usize,
1811    /// Optional metadata filter
1812    #[serde(skip_serializing_if = "Option::is_none")]
1813    pub filter: Option<serde_json::Value>,
1814    /// Optional text query for hybrid/fulltext search
1815    #[serde(skip_serializing_if = "Option::is_none")]
1816    pub text_query: Option<String>,
1817    /// Distance metric
1818    #[serde(default = "default_distance_metric")]
1819    pub distance_metric: String,
1820    /// Whether to actually execute the query for actual stats
1821    #[serde(default)]
1822    pub execute: bool,
1823    /// Include verbose output
1824    #[serde(default)]
1825    pub verbose: bool,
1826}
1827
1828fn default_distance_metric() -> String {
1829    "cosine".to_string()
1830}
1831
1832impl QueryExplainRequest {
1833    /// Create a new explain request for a vector search
1834    pub fn vector_search(vector: Vec<f32>, top_k: usize) -> Self {
1835        Self {
1836            query_type: ExplainQueryType::VectorSearch,
1837            vector: Some(vector),
1838            top_k,
1839            filter: None,
1840            text_query: None,
1841            distance_metric: "cosine".to_string(),
1842            execute: false,
1843            verbose: false,
1844        }
1845    }
1846
1847    /// Create a new explain request for a full-text search
1848    pub fn fulltext_search(text_query: impl Into<String>, top_k: usize) -> Self {
1849        Self {
1850            query_type: ExplainQueryType::FullTextSearch,
1851            vector: None,
1852            top_k,
1853            filter: None,
1854            text_query: Some(text_query.into()),
1855            distance_metric: "bm25".to_string(),
1856            execute: false,
1857            verbose: false,
1858        }
1859    }
1860
1861    /// Create a new explain request for a hybrid search
1862    pub fn hybrid_search(vector: Vec<f32>, text_query: impl Into<String>, top_k: usize) -> Self {
1863        Self {
1864            query_type: ExplainQueryType::HybridSearch,
1865            vector: Some(vector),
1866            top_k,
1867            filter: None,
1868            text_query: Some(text_query.into()),
1869            distance_metric: "hybrid".to_string(),
1870            execute: false,
1871            verbose: false,
1872        }
1873    }
1874
1875    /// Add a filter to the explain request
1876    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1877        self.filter = Some(filter);
1878        self
1879    }
1880
1881    /// Set the distance metric
1882    pub fn with_distance_metric(mut self, metric: impl Into<String>) -> Self {
1883        self.distance_metric = metric.into();
1884        self
1885    }
1886
1887    /// Execute the query to get actual stats
1888    pub fn with_execution(mut self) -> Self {
1889        self.execute = true;
1890        self
1891    }
1892
1893    /// Enable verbose output
1894    pub fn with_verbose(mut self) -> Self {
1895        self.verbose = true;
1896        self
1897    }
1898}
1899
1900/// A stage in query execution
1901#[derive(Debug, Clone, Serialize, Deserialize)]
1902pub struct ExecutionStage {
1903    /// Stage name
1904    pub name: String,
1905    /// Stage description
1906    pub description: String,
1907    /// Stage order (1-based)
1908    pub order: u32,
1909    /// Estimated input rows
1910    pub estimated_input: u64,
1911    /// Estimated output rows
1912    pub estimated_output: u64,
1913    /// Estimated cost for this stage
1914    pub estimated_cost: f64,
1915    /// Stage-specific details
1916    #[serde(default)]
1917    pub details: HashMap<String, serde_json::Value>,
1918}
1919
1920/// Cost estimation
1921#[derive(Debug, Clone, Serialize, Deserialize)]
1922pub struct CostEstimate {
1923    /// Total estimated cost (abstract units)
1924    pub total_cost: f64,
1925    /// Estimated execution time in milliseconds
1926    pub estimated_time_ms: u64,
1927    /// Estimated memory usage in bytes
1928    pub estimated_memory_bytes: u64,
1929    /// Estimated I/O operations
1930    pub estimated_io_ops: u64,
1931    /// Cost breakdown by component
1932    #[serde(default)]
1933    pub cost_breakdown: HashMap<String, f64>,
1934    /// Confidence level (0.0-1.0)
1935    pub confidence: f64,
1936}
1937
1938/// Actual execution statistics (when execute=true)
1939#[derive(Debug, Clone, Serialize, Deserialize)]
1940pub struct ActualStats {
1941    /// Actual execution time in milliseconds
1942    pub execution_time_ms: u64,
1943    /// Actual results returned
1944    pub results_returned: usize,
1945    /// Vectors scanned
1946    pub vectors_scanned: u64,
1947    /// Vectors after filter
1948    pub vectors_after_filter: u64,
1949    /// Index lookups performed
1950    pub index_lookups: u64,
1951    /// Cache hits
1952    pub cache_hits: u64,
1953    /// Cache misses
1954    pub cache_misses: u64,
1955    /// Memory used in bytes
1956    pub memory_used_bytes: u64,
1957}
1958
1959/// Performance recommendation
1960#[derive(Debug, Clone, Serialize, Deserialize)]
1961pub struct Recommendation {
1962    /// Recommendation type
1963    pub recommendation_type: String,
1964    /// Priority (high, medium, low)
1965    pub priority: String,
1966    /// Recommendation description
1967    pub description: String,
1968    /// Expected improvement
1969    pub expected_improvement: String,
1970    /// How to implement
1971    pub implementation: String,
1972}
1973
1974/// Index selection details
1975#[derive(Debug, Clone, Serialize, Deserialize)]
1976pub struct IndexSelection {
1977    /// Index type that will be used
1978    pub index_type: String,
1979    /// Why this index was selected
1980    pub selection_reason: String,
1981    /// Alternative indexes considered
1982    #[serde(default)]
1983    pub alternatives_considered: Vec<IndexAlternative>,
1984    /// Index configuration
1985    #[serde(default)]
1986    pub index_config: HashMap<String, serde_json::Value>,
1987    /// Index statistics
1988    pub index_stats: IndexStatistics,
1989}
1990
1991/// Alternative index that was considered
1992#[derive(Debug, Clone, Serialize, Deserialize)]
1993pub struct IndexAlternative {
1994    /// Index type
1995    pub index_type: String,
1996    /// Why it wasn't selected
1997    pub rejection_reason: String,
1998    /// Estimated cost if this index was used
1999    pub estimated_cost: f64,
2000}
2001
2002/// Index statistics
2003#[derive(Debug, Clone, Serialize, Deserialize)]
2004pub struct IndexStatistics {
2005    /// Total vectors in index
2006    pub vector_count: u64,
2007    /// Vector dimension
2008    pub dimension: usize,
2009    /// Index memory usage (estimated)
2010    pub memory_bytes: u64,
2011    /// Index build time (if available)
2012    #[serde(skip_serializing_if = "Option::is_none")]
2013    pub build_time_ms: Option<u64>,
2014    /// Last updated timestamp
2015    #[serde(skip_serializing_if = "Option::is_none")]
2016    pub last_updated: Option<u64>,
2017}
2018
2019/// Query parameters for reference
2020#[derive(Debug, Clone, Serialize, Deserialize)]
2021pub struct QueryParams {
2022    /// Number of results requested
2023    pub top_k: usize,
2024    /// Whether a filter was applied
2025    pub has_filter: bool,
2026    /// Filter complexity level
2027    pub filter_complexity: String,
2028    /// Vector dimension (if applicable)
2029    #[serde(skip_serializing_if = "Option::is_none")]
2030    pub vector_dimension: Option<usize>,
2031    /// Distance metric used
2032    pub distance_metric: String,
2033    /// Text query length (if applicable)
2034    #[serde(skip_serializing_if = "Option::is_none")]
2035    pub text_query_length: Option<usize>,
2036}
2037
2038/// Query explain response - detailed execution plan
2039#[derive(Debug, Clone, Serialize, Deserialize)]
2040pub struct QueryExplainResponse {
2041    /// Query type being explained
2042    pub query_type: ExplainQueryType,
2043    /// Namespace being queried
2044    pub namespace: String,
2045    /// Index selection information
2046    pub index_selection: IndexSelection,
2047    /// Query execution stages
2048    pub stages: Vec<ExecutionStage>,
2049    /// Cost estimates
2050    pub cost_estimate: CostEstimate,
2051    /// Actual execution stats (if execute=true)
2052    #[serde(skip_serializing_if = "Option::is_none")]
2053    pub actual_stats: Option<ActualStats>,
2054    /// Performance recommendations
2055    #[serde(default)]
2056    pub recommendations: Vec<Recommendation>,
2057    /// Query plan summary
2058    pub summary: String,
2059    /// Raw query parameters
2060    pub query_params: QueryParams,
2061}
2062
2063// ============================================================================
2064// Text Auto-Embedding Types
2065// ============================================================================
2066
2067/// Supported embedding models for text-based operations.
2068#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
2069#[serde(rename_all = "kebab-case")]
2070pub enum EmbeddingModel {
2071    /// BGE-large — Best quality, server default (1024 dimensions)
2072    #[default]
2073    BgeLarge,
2074    /// MiniLM-L6 — Fast, good quality (384 dimensions)
2075    Minilm,
2076    /// BGE-small — Balanced performance (384 dimensions)
2077    BgeSmall,
2078    /// E5-small — High quality (384 dimensions)
2079    E5Small,
2080    /// ModernBERT-embed-base (nomic-ai) — 768 dimensions, MRL, 8192 tokens
2081    #[serde(rename = "modernbert-embed-base")]
2082    ModernBertEmbedBase,
2083    /// GTE-ModernBERT-base (Alibaba-NLP) — 768 dimensions, MTEB retrieval 64.38
2084    #[serde(rename = "gte-modernbert-base")]
2085    GteModernBertBase,
2086}
2087
2088/// A text document to upsert with automatic embedding generation.
2089#[derive(Debug, Clone, Serialize, Deserialize)]
2090pub struct TextDocument {
2091    /// Unique identifier for the document.
2092    pub id: String,
2093    /// Raw text content to be embedded.
2094    pub text: String,
2095    /// Optional metadata for the document.
2096    #[serde(skip_serializing_if = "Option::is_none")]
2097    pub metadata: Option<HashMap<String, serde_json::Value>>,
2098    /// Optional TTL in seconds.
2099    #[serde(skip_serializing_if = "Option::is_none")]
2100    pub ttl_seconds: Option<u64>,
2101}
2102
2103impl TextDocument {
2104    /// Create a new text document with the given ID and text.
2105    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
2106        Self {
2107            id: id.into(),
2108            text: text.into(),
2109            metadata: None,
2110            ttl_seconds: None,
2111        }
2112    }
2113
2114    /// Add metadata to this document.
2115    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
2116        self.metadata = Some(metadata);
2117        self
2118    }
2119
2120    /// Set a TTL on this document.
2121    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
2122        self.ttl_seconds = Some(ttl_seconds);
2123        self
2124    }
2125}
2126
2127/// Request to upsert text documents with automatic embedding.
2128#[derive(Debug, Clone, Serialize, Deserialize)]
2129pub struct UpsertTextRequest {
2130    /// Documents to upsert.
2131    pub documents: Vec<TextDocument>,
2132    /// Embedding model to use (default: minilm).
2133    #[serde(skip_serializing_if = "Option::is_none")]
2134    pub model: Option<EmbeddingModel>,
2135}
2136
2137impl UpsertTextRequest {
2138    /// Create a new upsert-text request.
2139    pub fn new(documents: Vec<TextDocument>) -> Self {
2140        Self {
2141            documents,
2142            model: None,
2143        }
2144    }
2145
2146    /// Set the embedding model.
2147    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2148        self.model = Some(model);
2149        self
2150    }
2151}
2152
2153/// Response from a text upsert operation.
2154#[derive(Debug, Clone, Serialize, Deserialize)]
2155pub struct TextUpsertResponse {
2156    /// Number of documents upserted.
2157    pub upserted_count: u64,
2158    /// Approximate number of tokens processed.
2159    pub tokens_processed: u64,
2160    /// Embedding model used.
2161    pub model: EmbeddingModel,
2162    /// Time spent generating embeddings in milliseconds.
2163    pub embedding_time_ms: u64,
2164}
2165
2166/// A single text search result.
2167#[derive(Debug, Clone, Serialize, Deserialize)]
2168pub struct TextSearchResult {
2169    /// Document ID.
2170    pub id: String,
2171    /// Similarity score.
2172    pub score: f32,
2173    /// Original text (if `include_text` was true).
2174    #[serde(skip_serializing_if = "Option::is_none")]
2175    pub text: Option<String>,
2176    /// Document metadata.
2177    #[serde(skip_serializing_if = "Option::is_none")]
2178    pub metadata: Option<HashMap<String, serde_json::Value>>,
2179    /// Vector values (if `include_vectors` was true).
2180    #[serde(skip_serializing_if = "Option::is_none")]
2181    pub vector: Option<Vec<f32>>,
2182}
2183
2184/// Request to query using natural language text with automatic embedding.
2185#[derive(Debug, Clone, Serialize, Deserialize)]
2186pub struct QueryTextRequest {
2187    /// Query text.
2188    pub text: String,
2189    /// Number of results to return.
2190    pub top_k: u32,
2191    /// Optional metadata filter.
2192    #[serde(skip_serializing_if = "Option::is_none")]
2193    pub filter: Option<serde_json::Value>,
2194    /// Whether to include the original text in results.
2195    pub include_text: bool,
2196    /// Whether to include vectors in results.
2197    pub include_vectors: bool,
2198    /// Embedding model to use (default: minilm).
2199    #[serde(skip_serializing_if = "Option::is_none")]
2200    pub model: Option<EmbeddingModel>,
2201}
2202
2203impl QueryTextRequest {
2204    /// Create a new text query request.
2205    pub fn new(text: impl Into<String>, top_k: u32) -> Self {
2206        Self {
2207            text: text.into(),
2208            top_k,
2209            filter: None,
2210            include_text: true,
2211            include_vectors: false,
2212            model: None,
2213        }
2214    }
2215
2216    /// Add a metadata filter.
2217    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
2218        self.filter = Some(filter);
2219        self
2220    }
2221
2222    /// Set whether to include the original text in results.
2223    pub fn include_text(mut self, include: bool) -> Self {
2224        self.include_text = include;
2225        self
2226    }
2227
2228    /// Set whether to include vectors in results.
2229    pub fn include_vectors(mut self, include: bool) -> Self {
2230        self.include_vectors = include;
2231        self
2232    }
2233
2234    /// Set the embedding model.
2235    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2236        self.model = Some(model);
2237        self
2238    }
2239}
2240
2241/// Response from a text query operation.
2242#[derive(Debug, Clone, Serialize, Deserialize)]
2243pub struct TextQueryResponse {
2244    /// Search results.
2245    pub results: Vec<TextSearchResult>,
2246    /// Embedding model used.
2247    pub model: EmbeddingModel,
2248    /// Time spent generating the query embedding in milliseconds.
2249    pub embedding_time_ms: u64,
2250    /// Time spent searching in milliseconds.
2251    pub search_time_ms: u64,
2252}
2253
2254/// Request to execute multiple text queries with automatic embedding in a single call.
2255#[derive(Debug, Clone, Serialize, Deserialize)]
2256pub struct BatchQueryTextRequest {
2257    /// Text queries.
2258    pub queries: Vec<String>,
2259    /// Number of results per query.
2260    pub top_k: u32,
2261    /// Optional metadata filter applied to all queries.
2262    #[serde(skip_serializing_if = "Option::is_none")]
2263    pub filter: Option<serde_json::Value>,
2264    /// Whether to include vectors in results.
2265    pub include_vectors: bool,
2266    /// Embedding model to use (default: minilm).
2267    #[serde(skip_serializing_if = "Option::is_none")]
2268    pub model: Option<EmbeddingModel>,
2269}
2270
2271impl BatchQueryTextRequest {
2272    /// Create a new batch text query request.
2273    pub fn new(queries: Vec<String>, top_k: u32) -> Self {
2274        Self {
2275            queries,
2276            top_k,
2277            filter: None,
2278            include_vectors: false,
2279            model: None,
2280        }
2281    }
2282}
2283
2284/// Response from a batch text query operation.
2285#[derive(Debug, Clone, Serialize, Deserialize)]
2286pub struct BatchQueryTextResponse {
2287    /// Results for each query (in the same order as the request).
2288    pub results: Vec<Vec<TextSearchResult>>,
2289    /// Embedding model used.
2290    pub model: EmbeddingModel,
2291    /// Time spent generating all embeddings in milliseconds.
2292    pub embedding_time_ms: u64,
2293    /// Time spent on all searches in milliseconds.
2294    pub search_time_ms: u64,
2295}
2296
2297// ============================================================================
2298// Fetch by ID Types
2299// ============================================================================
2300
2301/// Request to fetch vectors by their IDs.
2302#[derive(Debug, Clone, Serialize, Deserialize)]
2303pub struct FetchRequest {
2304    /// IDs of vectors to fetch.
2305    pub ids: Vec<String>,
2306    /// Whether to include vector values.
2307    pub include_values: bool,
2308    /// Whether to include metadata.
2309    pub include_metadata: bool,
2310}
2311
2312impl FetchRequest {
2313    /// Create a new fetch request.
2314    pub fn new(ids: Vec<String>) -> Self {
2315        Self {
2316            ids,
2317            include_values: true,
2318            include_metadata: true,
2319        }
2320    }
2321}
2322
2323/// Response from a fetch-by-ID operation.
2324#[derive(Debug, Clone, Serialize, Deserialize)]
2325pub struct FetchResponse {
2326    /// Fetched vectors.
2327    pub vectors: Vec<Vector>,
2328}
2329
2330// ============================================================================
2331// Namespace Management Types
2332// ============================================================================
2333
2334/// Request to create a new namespace.
2335#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2336pub struct CreateNamespaceRequest {
2337    /// Vector dimensions (inferred from first upsert if omitted).
2338    #[serde(rename = "dimension", skip_serializing_if = "Option::is_none")]
2339    pub dimensions: Option<u32>,
2340    /// Index type (e.g. "hnsw", "flat").
2341    #[serde(skip_serializing_if = "Option::is_none")]
2342    pub index_type: Option<String>,
2343    /// Arbitrary namespace metadata.
2344    #[serde(skip_serializing_if = "Option::is_none")]
2345    pub metadata: Option<HashMap<String, serde_json::Value>>,
2346}
2347
2348impl CreateNamespaceRequest {
2349    /// Create a minimal request (server picks sensible defaults).
2350    pub fn new() -> Self {
2351        Self::default()
2352    }
2353
2354    /// Set the vector dimensions.
2355    pub fn with_dimensions(mut self, dimensions: u32) -> Self {
2356        self.dimensions = Some(dimensions);
2357        self
2358    }
2359
2360    /// Set the index type.
2361    pub fn with_index_type(mut self, index_type: impl Into<String>) -> Self {
2362        self.index_type = Some(index_type.into());
2363        self
2364    }
2365}
2366
2367/// Request body for `PUT /v1/namespaces/:namespace` — upsert semantics (v0.6.0).
2368///
2369/// Creates the namespace if it does not exist, or updates its configuration
2370/// if it already exists.  Requires `Scope::Write`.
2371#[derive(Debug, Clone, Serialize, Deserialize)]
2372pub struct ConfigureNamespaceRequest {
2373    /// Vector dimension.  Required on first creation; must match on subsequent calls.
2374    pub dimension: usize,
2375    /// Distance metric (defaults to cosine when omitted).
2376    #[serde(skip_serializing_if = "Option::is_none")]
2377    pub distance: Option<DistanceMetric>,
2378}
2379
2380impl ConfigureNamespaceRequest {
2381    /// Create a new configure-namespace request with the given dimension.
2382    pub fn new(dimension: usize) -> Self {
2383        Self {
2384            dimension,
2385            distance: None,
2386        }
2387    }
2388
2389    /// Set the distance metric.
2390    pub fn with_distance(mut self, distance: DistanceMetric) -> Self {
2391        self.distance = Some(distance);
2392        self
2393    }
2394}
2395
2396/// Response from `PUT /v1/namespaces/:namespace`.
2397#[derive(Debug, Clone, Serialize, Deserialize)]
2398pub struct ConfigureNamespaceResponse {
2399    /// Namespace name.
2400    pub namespace: String,
2401    /// Vector dimension.
2402    pub dimension: usize,
2403    /// Distance metric in use.
2404    pub distance: DistanceMetric,
2405    /// `true` if the namespace was newly created; `false` if it already existed.
2406    pub created: bool,
2407}
2408
2409// ============================================================================
2410// Memory Knowledge Graph Types (CE-5 / SDK-9)
2411// ============================================================================
2412
2413/// Edge type for memory knowledge graph relationships (CE-5).
2414#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2415#[serde(rename_all = "snake_case")]
2416pub enum EdgeType {
2417    /// Cosine similarity ≥ 0.85 — two memories are semantically similar.
2418    RelatedTo,
2419    /// Both memories reference the same named entity (CE-4 tags).
2420    SharesEntity,
2421    /// Temporal ordering — source was created before target.
2422    Precedes,
2423    /// Explicit user/agent-created link.
2424    #[default]
2425    LinkedBy,
2426}
2427
2428/// A directed edge in the memory knowledge graph.
2429#[derive(Debug, Clone, Serialize, Deserialize)]
2430pub struct GraphEdge {
2431    /// Unique edge identifier.
2432    pub id: String,
2433    /// Source memory ID.
2434    pub source_id: String,
2435    /// Target memory ID.
2436    pub target_id: String,
2437    /// Relationship type between the two memories.
2438    pub edge_type: EdgeType,
2439    /// Edge weight (0.0–1.0). For `RelatedTo` this is the cosine similarity score.
2440    pub weight: f64,
2441    /// Unix timestamp of edge creation.
2442    pub created_at: i64,
2443}
2444
2445/// A node (memory) in the knowledge graph traversal result.
2446#[derive(Debug, Clone, Serialize, Deserialize)]
2447pub struct GraphNode {
2448    /// Memory identifier.
2449    pub memory_id: String,
2450    /// First 200 characters of memory content.
2451    pub content_preview: String,
2452    /// Memory importance score.
2453    pub importance: f64,
2454    /// Traversal depth from the root node (root = 0).
2455    pub depth: u32,
2456}
2457
2458/// Graph traversal result from `GET /v1/memories/{id}/graph`.
2459#[derive(Debug, Clone, Serialize, Deserialize)]
2460pub struct MemoryGraph {
2461    /// The root memory ID from which traversal started.
2462    pub root_id: String,
2463    /// Maximum traversal depth used.
2464    pub depth: u32,
2465    /// All memory nodes reachable within the requested depth.
2466    pub nodes: Vec<GraphNode>,
2467    /// All edges connecting the returned nodes.
2468    pub edges: Vec<GraphEdge>,
2469}
2470
2471/// Shortest path between two memories from `GET /v1/memories/{id}/path`.
2472#[derive(Debug, Clone, Serialize, Deserialize)]
2473pub struct GraphPath {
2474    /// Starting memory ID.
2475    pub source_id: String,
2476    /// Destination memory ID.
2477    pub target_id: String,
2478    /// Ordered list of memory IDs from source to target (inclusive).
2479    pub path: Vec<String>,
2480    /// Number of edges traversed (`path.len() - 1`). `-1` if no path exists.
2481    pub hops: i32,
2482    /// Edges along the path, in traversal order.
2483    pub edges: Vec<GraphEdge>,
2484}
2485
2486/// Request body for `POST /v1/memories/{id}/links`.
2487#[derive(Debug, Clone, Serialize, Deserialize)]
2488pub struct GraphLinkRequest {
2489    /// Target memory ID to link to.
2490    pub target_id: String,
2491    /// Edge type — must be `LinkedBy` for explicit links.
2492    pub edge_type: EdgeType,
2493}
2494
2495/// Response from `POST /v1/memories/{id}/links`.
2496#[derive(Debug, Clone, Serialize, Deserialize)]
2497pub struct GraphLinkResponse {
2498    /// The newly created edge.
2499    pub edge: GraphEdge,
2500}
2501
2502/// Agent graph export from `GET /v1/agents/{id}/graph/export`.
2503#[derive(Debug, Clone, Serialize, Deserialize)]
2504pub struct GraphExport {
2505    /// Agent whose graph was exported.
2506    pub agent_id: String,
2507    /// Export format: `json`, `graphml`, or `csv`.
2508    pub format: String,
2509    /// Serialised graph in the requested format.
2510    pub data: String,
2511    /// Total number of memory nodes in the export.
2512    pub node_count: u64,
2513    /// Total number of edges in the export.
2514    pub edge_count: u64,
2515}
2516
2517/// Options for [`DakeraClient::memory_graph`].
2518#[derive(Debug, Clone, Default)]
2519pub struct GraphOptions {
2520    /// Maximum traversal depth (default: 1, max: 3).
2521    pub depth: Option<u32>,
2522    /// Filter by edge types. `None` returns all types.
2523    pub types: Option<Vec<EdgeType>>,
2524}
2525
2526impl GraphOptions {
2527    /// Create default options.
2528    pub fn new() -> Self {
2529        Self::default()
2530    }
2531
2532    /// Set traversal depth.
2533    pub fn depth(mut self, depth: u32) -> Self {
2534        self.depth = Some(depth);
2535        self
2536    }
2537
2538    /// Filter by edge types.
2539    pub fn types(mut self, types: Vec<EdgeType>) -> Self {
2540        self.types = Some(types);
2541        self
2542    }
2543}
2544
2545// ============================================================================
2546// CE-4: GLiNER Entity Extraction Types
2547// ============================================================================
2548
2549/// Configuration for namespace-level entity extraction (CE-4).
2550#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2551pub struct NamespaceNerConfig {
2552    pub extract_entities: bool,
2553    #[serde(skip_serializing_if = "Option::is_none")]
2554    pub entity_types: Option<Vec<String>>,
2555}
2556
2557/// A single extracted entity from GLiNER or rule-based pipeline.
2558#[derive(Debug, Clone, Serialize, Deserialize)]
2559pub struct ExtractedEntity {
2560    pub entity_type: String,
2561    pub value: String,
2562    pub score: f64,
2563}
2564
2565/// Response from POST /v1/memories/extract
2566#[derive(Debug, Clone, Serialize, Deserialize)]
2567pub struct EntityExtractionResponse {
2568    pub entities: Vec<ExtractedEntity>,
2569}
2570
2571/// Response from GET /v1/memory/entities/:id
2572#[derive(Debug, Clone, Serialize, Deserialize)]
2573pub struct MemoryEntitiesResponse {
2574    pub memory_id: String,
2575    pub entities: Vec<ExtractedEntity>,
2576}
2577
2578// ============================================================================
2579// Memory Feedback Loop (INT-1)
2580// ============================================================================
2581
2582/// Feedback signal for memory active learning (INT-1).
2583///
2584/// - `upvote`: Boost importance ×1.15, capped at 1.0.
2585/// - `downvote`: Penalise importance ×0.85, floor 0.0.
2586/// - `flag`: Mark as irrelevant — sets `decay_flag=true`, no immediate importance change.
2587/// - `positive`: Backward-compatible alias for `upvote`.
2588/// - `negative`: Backward-compatible alias for `downvote`.
2589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2590#[serde(rename_all = "lowercase")]
2591pub enum FeedbackSignal {
2592    Upvote,
2593    Downvote,
2594    Flag,
2595    Positive,
2596    Negative,
2597}
2598
2599/// A single recorded feedback event stored in memory metadata (INT-1).
2600#[derive(Debug, Clone, Serialize, Deserialize)]
2601pub struct FeedbackHistoryEntry {
2602    pub signal: FeedbackSignal,
2603    /// Unix timestamp (seconds) when feedback was submitted.
2604    pub timestamp: u64,
2605    pub old_importance: f32,
2606    pub new_importance: f32,
2607}
2608
2609/// Request body for `POST /v1/memories/:id/feedback` (INT-1).
2610#[derive(Debug, Clone, Serialize, Deserialize)]
2611pub struct MemoryFeedbackBody {
2612    pub agent_id: String,
2613    pub signal: FeedbackSignal,
2614}
2615
2616/// Request body for `PATCH /v1/memories/:id/importance` (INT-1).
2617#[derive(Debug, Clone, Serialize, Deserialize)]
2618pub struct MemoryImportancePatch {
2619    pub agent_id: String,
2620    pub importance: f32,
2621}
2622
2623/// Response from `POST /v1/memories/:id/feedback` and `PATCH /v1/memories/:id/importance` (INT-1).
2624#[derive(Debug, Clone, Serialize, Deserialize)]
2625pub struct FeedbackResponse {
2626    pub memory_id: String,
2627    /// New importance score after the feedback was applied (0.0–1.0).
2628    pub new_importance: f32,
2629    pub signal: FeedbackSignal,
2630}
2631
2632/// Response from `GET /v1/memories/:id/feedback` (INT-1).
2633#[derive(Debug, Clone, Serialize, Deserialize)]
2634pub struct FeedbackHistoryResponse {
2635    pub memory_id: String,
2636    /// Ordered list of feedback events (oldest first, capped at 100).
2637    pub entries: Vec<FeedbackHistoryEntry>,
2638}
2639
2640/// Response from `GET /v1/agents/:id/feedback/summary` (INT-1).
2641#[derive(Debug, Clone, Serialize, Deserialize)]
2642pub struct AgentFeedbackSummary {
2643    pub agent_id: String,
2644    pub upvotes: u64,
2645    pub downvotes: u64,
2646    pub flags: u64,
2647    pub total_feedback: u64,
2648    /// Weighted-average importance across all non-expired memories (0.0–1.0).
2649    pub health_score: f32,
2650}
2651
2652/// Response from `GET /v1/feedback/health` (INT-1).
2653#[derive(Debug, Clone, Serialize, Deserialize)]
2654pub struct FeedbackHealthResponse {
2655    pub agent_id: String,
2656    /// Mean importance of all non-expired memories (0.0–1.0). Higher = healthier.
2657    pub health_score: f32,
2658    pub memory_count: usize,
2659    pub avg_importance: f32,
2660}
2661
2662// ============================================================================
2663// T-I-F Reliability Scoring (Phase 3 T-I-F RFC)
2664// ============================================================================
2665
2666/// Reliability classification label from a [`TifScore`].
2667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2668#[serde(rename_all = "snake_case")]
2669pub enum TifClassification {
2670    /// Majority of feedback is negative — the memory likely contains incorrect information.
2671    SurfaceContradiction,
2672    /// Majority of feedback is uncertain — ask the user for clarification before reusing.
2673    AskClarification,
2674    /// Strong positive feedback signal — safe to reuse without additional verification.
2675    ConfidentReuse,
2676    /// Mixed or weak signals — verify the memory before acting on it.
2677    VerifyBeforeUse,
2678}
2679
2680impl TifClassification {
2681    /// Stable string label matching the Python/JS/Go SDK classification strings.
2682    pub fn as_str(&self) -> &'static str {
2683        match self {
2684            Self::SurfaceContradiction => "surface_contradiction",
2685            Self::AskClarification => "ask_clarification",
2686            Self::ConfidentReuse => "confident_reuse",
2687            Self::VerifyBeforeUse => "verify_before_use",
2688        }
2689    }
2690}
2691
2692/// Truth-Indeterminacy-Falsity reliability score for a memory (T-I-F RFC Phase 3).
2693///
2694/// All three proportions (`truth`, `indeterminacy`, `falsity`) sum to 1.0.
2695/// Build via [`TifScore::from_feedback_history`] or [`TifScore::from_metadata`].
2696#[derive(Debug, Clone, Serialize, Deserialize)]
2697pub struct TifScore {
2698    /// Proportion of positive feedback signals (`upvote` / `positive`).
2699    pub truth: f64,
2700    /// Proportion of uncertainty signals (`flag`).
2701    pub indeterminacy: f64,
2702    /// Proportion of negative feedback signals (`downvote` / `negative`).
2703    pub falsity: f64,
2704    /// Total feedback events used to compute this score.
2705    pub feedback_count: u64,
2706    /// Human-readable reliability classification.
2707    pub classification: TifClassification,
2708}
2709
2710fn classify_tif(truth: f64, indeterminacy: f64, falsity: f64) -> TifClassification {
2711    if falsity >= 0.5 {
2712        TifClassification::SurfaceContradiction
2713    } else if indeterminacy >= 0.5 {
2714        TifClassification::AskClarification
2715    } else if truth >= 0.7 {
2716        TifClassification::ConfidentReuse
2717    } else {
2718        TifClassification::VerifyBeforeUse
2719    }
2720}
2721
2722impl TifScore {
2723    /// Compute a [`TifScore`] from a memory's [`FeedbackHistoryResponse`].
2724    ///
2725    /// Signals are bucketed as:
2726    /// - [`FeedbackSignal::Upvote`] / [`FeedbackSignal::Positive`] → truth
2727    /// - [`FeedbackSignal::Downvote`] / [`FeedbackSignal::Negative`] → falsity
2728    /// - [`FeedbackSignal::Flag`] → indeterminacy
2729    ///
2730    /// With no feedback the score is `{ truth: 0.0, indeterminacy: 1.0, falsity: 0.0, feedback_count: 0 }`.
2731    pub fn from_feedback_history(history: &FeedbackHistoryResponse) -> Self {
2732        let mut upvotes: u64 = 0;
2733        let mut downvotes: u64 = 0;
2734        let mut flags: u64 = 0;
2735        for entry in &history.entries {
2736            match entry.signal {
2737                FeedbackSignal::Upvote | FeedbackSignal::Positive => upvotes += 1,
2738                FeedbackSignal::Downvote | FeedbackSignal::Negative => downvotes += 1,
2739                FeedbackSignal::Flag => flags += 1,
2740            }
2741        }
2742        let total = upvotes + downvotes + flags;
2743        if total == 0 {
2744            return Self {
2745                truth: 0.0,
2746                indeterminacy: 1.0,
2747                falsity: 0.0,
2748                feedback_count: 0,
2749                classification: TifClassification::AskClarification,
2750            };
2751        }
2752        let total_f = total as f64;
2753        let base_indeterminacy = if total < 3 {
2754            (3 - total) as f64 * 0.25
2755        } else {
2756            0.0
2757        };
2758        let mut truth = upvotes as f64 / total_f;
2759        let mut falsity = downvotes as f64 / total_f;
2760        let mut indeterminacy = flags as f64 / total_f + base_indeterminacy;
2761        let sum = truth + falsity + indeterminacy;
2762        truth /= sum;
2763        falsity /= sum;
2764        indeterminacy /= sum;
2765        Self {
2766            truth,
2767            indeterminacy,
2768            falsity,
2769            feedback_count: total,
2770            classification: classify_tif(truth, indeterminacy, falsity),
2771        }
2772    }
2773
2774    /// Deserialise a [`TifScore`] from a `metadata["reliability"]` map.
2775    ///
2776    /// Expected keys: `truth`, `indeterminacy`, `falsity`, `feedback_count` (snake_case).
2777    pub fn from_metadata(data: &serde_json::Value) -> Option<Self> {
2778        let truth = data["truth"].as_f64()?;
2779        let indeterminacy = data["indeterminacy"].as_f64()?;
2780        let falsity = data["falsity"].as_f64()?;
2781        let feedback_count = data["feedback_count"].as_u64().unwrap_or(0);
2782        Some(Self {
2783            truth,
2784            indeterminacy,
2785            falsity,
2786            feedback_count,
2787            classification: classify_tif(truth, indeterminacy, falsity),
2788        })
2789    }
2790}
2791
2792#[cfg(test)]
2793mod tif_tests {
2794    use super::*;
2795
2796    fn make_history(signals: &[&str]) -> FeedbackHistoryResponse {
2797        FeedbackHistoryResponse {
2798            memory_id: "test-mem".to_string(),
2799            entries: signals
2800                .iter()
2801                .map(|s| {
2802                    let signal = match *s {
2803                        "upvote" => FeedbackSignal::Upvote,
2804                        "downvote" => FeedbackSignal::Downvote,
2805                        "flag" => FeedbackSignal::Flag,
2806                        "positive" => FeedbackSignal::Positive,
2807                        "negative" => FeedbackSignal::Negative,
2808                        other => panic!("unknown signal: {other}"),
2809                    };
2810                    FeedbackHistoryEntry {
2811                        signal,
2812                        timestamp: 0,
2813                        old_importance: 0.5,
2814                        new_importance: 0.5,
2815                    }
2816                })
2817                .collect(),
2818        }
2819    }
2820
2821    #[test]
2822    fn no_feedback_max_indeterminacy() {
2823        let score = TifScore::from_feedback_history(&make_history(&[]));
2824        assert_eq!(score.truth, 0.0);
2825        assert_eq!(score.indeterminacy, 1.0);
2826        assert_eq!(score.falsity, 0.0);
2827        assert_eq!(score.feedback_count, 0);
2828        assert_eq!(score.classification, TifClassification::AskClarification);
2829    }
2830
2831    #[test]
2832    fn all_upvotes() {
2833        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
2834        assert!((score.truth - 1.0).abs() < 1e-9);
2835        assert_eq!(score.feedback_count, 3);
2836        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2837    }
2838
2839    #[test]
2840    fn all_downvotes() {
2841        let score = TifScore::from_feedback_history(&make_history(&["downvote", "downvote"]));
2842        assert!((score.falsity - 0.8).abs() < 1e-9);
2843        assert!((score.indeterminacy - 0.2).abs() < 1e-9);
2844        assert_eq!(
2845            score.classification,
2846            TifClassification::SurfaceContradiction
2847        );
2848    }
2849
2850    #[test]
2851    fn all_flags() {
2852        let score = TifScore::from_feedback_history(&make_history(&["flag", "flag"]));
2853        assert!((score.indeterminacy - 1.0).abs() < 1e-9);
2854        assert_eq!(score.classification, TifClassification::AskClarification);
2855    }
2856
2857    #[test]
2858    fn mixed_signals() {
2859        let score = TifScore::from_feedback_history(&make_history(&[
2860            "upvote", "upvote", "upvote", "upvote", "downvote", "downvote", "flag", "flag", "flag",
2861            "flag",
2862        ]));
2863        assert!((score.truth - 0.4).abs() < 1e-9);
2864        assert!((score.falsity - 0.2).abs() < 1e-9);
2865        assert!((score.indeterminacy - 0.4).abs() < 1e-9);
2866        assert_eq!(score.feedback_count, 10);
2867    }
2868
2869    #[test]
2870    fn positive_alias() {
2871        let score =
2872            TifScore::from_feedback_history(&make_history(&["positive", "positive", "downvote"]));
2873        assert!((score.truth - 2.0 / 3.0).abs() < 1e-9);
2874        assert!((score.falsity - 1.0 / 3.0).abs() < 1e-9);
2875    }
2876
2877    #[test]
2878    fn negative_alias() {
2879        let score =
2880            TifScore::from_feedback_history(&make_history(&["upvote", "negative", "negative"]));
2881        assert!((score.falsity - 2.0 / 3.0).abs() < 1e-9);
2882    }
2883
2884    #[test]
2885    fn proportions_sum_to_one() {
2886        let score = TifScore::from_feedback_history(&make_history(&["upvote", "downvote", "flag"]));
2887        assert!((score.truth + score.indeterminacy + score.falsity - 1.0).abs() < 1e-9);
2888    }
2889
2890    #[test]
2891    fn classification_surface_contradiction() {
2892        let score = TifScore::from_feedback_history(&make_history(&[
2893            "downvote", "downvote", "downvote", "upvote", "upvote",
2894        ]));
2895        assert_eq!(
2896            score.classification,
2897            TifClassification::SurfaceContradiction
2898        );
2899    }
2900
2901    #[test]
2902    fn classification_verify_before_use() {
2903        // 2 upvotes, 2 downvotes, 3 flags → no dominant signal
2904        let score = TifScore::from_feedback_history(&make_history(&[
2905            "upvote", "upvote", "downvote", "downvote", "flag", "flag", "flag",
2906        ]));
2907        assert_eq!(score.classification, TifClassification::VerifyBeforeUse);
2908    }
2909
2910    #[test]
2911    fn falsity_priority_over_indeterminacy() {
2912        // 3 downvotes + 3 flags: both >= 0.5, falsity wins
2913        let score = TifScore::from_feedback_history(&make_history(&[
2914            "downvote", "downvote", "downvote", "flag", "flag", "flag",
2915        ]));
2916        assert_eq!(
2917            score.classification,
2918            TifClassification::SurfaceContradiction
2919        );
2920    }
2921
2922    #[test]
2923    fn from_metadata_round_trip() {
2924        use serde_json::json;
2925        let data =
2926            json!({ "truth": 0.75, "indeterminacy": 0.15, "falsity": 0.10, "feedback_count": 20 });
2927        let score = TifScore::from_metadata(&data).unwrap();
2928        assert!((score.truth - 0.75).abs() < 1e-9);
2929        assert_eq!(score.feedback_count, 20);
2930        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2931    }
2932
2933    #[test]
2934    fn from_metadata_missing_feedback_count() {
2935        use serde_json::json;
2936        let data = json!({ "truth": 0.8, "indeterminacy": 0.1, "falsity": 0.1 });
2937        let score = TifScore::from_metadata(&data).unwrap();
2938        assert_eq!(score.feedback_count, 0);
2939    }
2940
2941    #[test]
2942    fn classification_as_str() {
2943        assert_eq!(
2944            TifClassification::SurfaceContradiction.as_str(),
2945            "surface_contradiction"
2946        );
2947        assert_eq!(
2948            TifClassification::AskClarification.as_str(),
2949            "ask_clarification"
2950        );
2951        assert_eq!(
2952            TifClassification::ConfidentReuse.as_str(),
2953            "confident_reuse"
2954        );
2955        assert_eq!(
2956            TifClassification::VerifyBeforeUse.as_str(),
2957            "verify_before_use"
2958        );
2959    }
2960
2961    // ── Thin evidence ────────────────────────────────────────────────────
2962
2963    #[test]
2964    fn thin_evidence_single_upvote_not_confident() {
2965        let score = TifScore::from_feedback_history(&make_history(&["upvote"]));
2966        assert!((score.truth + score.indeterminacy + score.falsity - 1.0).abs() < 1e-9);
2967        assert!(score.indeterminacy > 0.0);
2968        assert!(score.truth < 0.70);
2969        assert_eq!(score.classification, TifClassification::VerifyBeforeUse);
2970    }
2971
2972    #[test]
2973    fn thin_evidence_two_upvotes_confident() {
2974        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote"]));
2975        assert!((score.truth - 0.8).abs() < 1e-9);
2976        assert!((score.indeterminacy - 0.2).abs() < 1e-9);
2977        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2978    }
2979
2980    #[test]
2981    fn thin_evidence_three_upvotes_no_base() {
2982        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
2983        assert!((score.truth - 1.0).abs() < 1e-9);
2984        assert!((score.indeterminacy - 0.0).abs() < 1e-9);
2985        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2986    }
2987
2988    // ── Golden vectors (canonical T-I-F v1 contract) ─────────────────────
2989
2990    #[test]
2991    fn golden_no_feedback() {
2992        let s = TifScore::from_feedback_history(&make_history(&[]));
2993        assert!((s.truth - 0.0).abs() < 1e-9);
2994        assert!((s.indeterminacy - 1.0).abs() < 1e-9);
2995        assert!((s.falsity - 0.0).abs() < 1e-9);
2996        assert_eq!(s.classification, TifClassification::AskClarification);
2997    }
2998
2999    #[test]
3000    fn golden_one_upvote() {
3001        let s = TifScore::from_feedback_history(&make_history(&["upvote"]));
3002        assert!((s.truth - 2.0 / 3.0).abs() < 1e-4);
3003        assert!((s.indeterminacy - 1.0 / 3.0).abs() < 1e-4);
3004        assert!((s.falsity - 0.0).abs() < 1e-9);
3005        assert_eq!(s.classification, TifClassification::VerifyBeforeUse);
3006    }
3007
3008    #[test]
3009    fn golden_two_upvotes() {
3010        let s = TifScore::from_feedback_history(&make_history(&["upvote", "upvote"]));
3011        assert!((s.truth - 0.8).abs() < 1e-9);
3012        assert!((s.indeterminacy - 0.2).abs() < 1e-9);
3013        assert!((s.falsity - 0.0).abs() < 1e-9);
3014        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3015    }
3016
3017    #[test]
3018    fn golden_three_upvotes() {
3019        let s = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
3020        assert!((s.truth - 1.0).abs() < 1e-9);
3021        assert!((s.indeterminacy - 0.0).abs() < 1e-9);
3022        assert!((s.falsity - 0.0).abs() < 1e-9);
3023        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3024    }
3025
3026    #[test]
3027    fn golden_two_downvotes() {
3028        let s = TifScore::from_feedback_history(&make_history(&["downvote", "downvote"]));
3029        assert!((s.truth - 0.0).abs() < 1e-9);
3030        assert!((s.indeterminacy - 0.2).abs() < 1e-9);
3031        assert!((s.falsity - 0.8).abs() < 1e-9);
3032        assert_eq!(s.classification, TifClassification::SurfaceContradiction);
3033    }
3034
3035    #[test]
3036    fn golden_two_flags() {
3037        let s = TifScore::from_feedback_history(&make_history(&["flag", "flag"]));
3038        assert!((s.truth - 0.0).abs() < 1e-9);
3039        assert!((s.indeterminacy - 1.0).abs() < 1e-9);
3040        assert!((s.falsity - 0.0).abs() < 1e-9);
3041        assert_eq!(s.classification, TifClassification::AskClarification);
3042    }
3043
3044    #[test]
3045    fn golden_8up_1down_1flag() {
3046        let s = TifScore::from_feedback_history(&make_history(&[
3047            "upvote", "upvote", "upvote", "upvote", "upvote", "upvote", "upvote", "upvote",
3048            "downvote", "flag",
3049        ]));
3050        assert!((s.truth - 0.8).abs() < 1e-9);
3051        assert!((s.indeterminacy - 0.1).abs() < 1e-9);
3052        assert!((s.falsity - 0.1).abs() < 1e-9);
3053        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3054    }
3055
3056    #[test]
3057    fn golden_3down_3flag() {
3058        let s = TifScore::from_feedback_history(&make_history(&[
3059            "downvote", "downvote", "downvote", "flag", "flag", "flag",
3060        ]));
3061        assert!((s.truth - 0.0).abs() < 1e-9);
3062        assert!((s.indeterminacy - 0.5).abs() < 1e-9);
3063        assert!((s.falsity - 0.5).abs() < 1e-9);
3064        assert_eq!(s.classification, TifClassification::SurfaceContradiction);
3065    }
3066}
3067
3068// ============================================================================
3069// ODE-2: GLiNER Entity Extraction (dakera-ode sidecar)
3070// ============================================================================
3071
3072/// A single entity extracted by the GLiNER model (ODE-2).
3073#[derive(Debug, Clone, Serialize, Deserialize)]
3074pub struct OdeEntity {
3075    /// Span text as it appears in the input.
3076    pub text: String,
3077    /// Entity type label (e.g. `"person"`, `"organization"`).
3078    pub label: String,
3079    /// Start character offset (inclusive) within the input text.
3080    pub start: usize,
3081    /// End character offset (exclusive) within the input text.
3082    pub end: usize,
3083    /// Confidence score in the range [0, 1].
3084    pub score: f32,
3085}
3086
3087/// Request body for `POST /ode/extract` (ODE-2).
3088#[derive(Debug, Clone, Serialize, Deserialize)]
3089pub struct ExtractEntitiesRequest {
3090    /// The text to extract entities from.
3091    pub content: String,
3092    /// Agent context for the extraction.
3093    pub agent_id: String,
3094    /// Optional memory ID to associate with the extraction.
3095    #[serde(skip_serializing_if = "Option::is_none")]
3096    pub memory_id: Option<String>,
3097    /// Optional list of entity type labels to extract.
3098    /// When omitted the ODE sidecar uses its default set.
3099    #[serde(skip_serializing_if = "Option::is_none")]
3100    pub entity_types: Option<Vec<String>>,
3101}
3102
3103/// Response from `POST /ode/extract` on the ODE sidecar (ODE-2).
3104#[derive(Debug, Clone, Serialize, Deserialize)]
3105pub struct ExtractEntitiesResponse {
3106    /// Extracted entities ordered by their start offset.
3107    pub entities: Vec<OdeEntity>,
3108    /// GLiNER model variant used for extraction.
3109    pub model: String,
3110    /// Wall-clock time taken by the ODE sidecar in milliseconds.
3111    pub processing_time_ms: u64,
3112}
3113
3114// ============================================================================
3115// KG-2: Graph Query & Export — response types
3116// ============================================================================
3117
3118/// Response from `GET /v1/knowledge/query` (KG-2).
3119#[derive(Debug, Clone, Serialize, Deserialize)]
3120pub struct KgQueryResponse {
3121    /// Agent whose graph was queried.
3122    pub agent_id: String,
3123    /// Number of unique memory node IDs referenced by the returned edges.
3124    pub node_count: usize,
3125    /// Number of edges returned.
3126    pub edge_count: usize,
3127    /// Matching edges, up to `limit`.
3128    pub edges: Vec<GraphEdge>,
3129}
3130
3131/// Response from `GET /v1/knowledge/path` (KG-2).
3132#[derive(Debug, Clone, Serialize, Deserialize)]
3133pub struct KgPathResponse {
3134    /// Agent whose graph was traversed.
3135    pub agent_id: String,
3136    /// Source memory ID.
3137    pub from_id: String,
3138    /// Target memory ID.
3139    pub to_id: String,
3140    /// Number of edges in the shortest path (0 if source == target).
3141    pub hop_count: usize,
3142    /// Ordered list of memory IDs from source to target (inclusive).
3143    pub path: Vec<String>,
3144}
3145
3146/// Response from `GET /v1/knowledge/export` with `format=json` (KG-2).
3147#[derive(Debug, Clone, Serialize, Deserialize)]
3148pub struct KgExportResponse {
3149    /// Agent whose graph was exported.
3150    pub agent_id: String,
3151    /// Export format used (`"json"` when this struct is deserialized).
3152    pub format: String,
3153    /// Total number of unique memory node IDs in the export.
3154    pub node_count: usize,
3155    /// Total number of edges in the export.
3156    pub edge_count: usize,
3157    /// All graph edges for the agent.
3158    pub edges: Vec<GraphEdge>,
3159}
3160
3161// ============================================================================
3162// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
3163// ============================================================================
3164
3165/// Per-namespace memory lifecycle policy (COG-1).
3166///
3167/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
3168/// All fields have sensible defaults; only override what you need.
3169///
3170/// Used by [`DakeraClient::get_memory_policy`] and
3171/// [`DakeraClient::set_memory_policy`].
3172#[derive(Debug, Clone, Serialize, Deserialize)]
3173pub struct MemoryPolicy {
3174    // Differential TTLs ------------------------------------------------------
3175    /// Default TTL for `working` memories in seconds (default: 14 400 = 4 h).
3176    #[serde(skip_serializing_if = "Option::is_none")]
3177    pub working_ttl_seconds: Option<u64>,
3178    /// Default TTL for `episodic` memories in seconds (default: 2 592 000 = 30 d).
3179    #[serde(skip_serializing_if = "Option::is_none")]
3180    pub episodic_ttl_seconds: Option<u64>,
3181    /// Default TTL for `semantic` memories in seconds (default: 31 536 000 = 365 d).
3182    #[serde(skip_serializing_if = "Option::is_none")]
3183    pub semantic_ttl_seconds: Option<u64>,
3184    /// Default TTL for `procedural` memories in seconds (default: 63 072 000 = 730 d).
3185    #[serde(skip_serializing_if = "Option::is_none")]
3186    pub procedural_ttl_seconds: Option<u64>,
3187
3188    // Decay curves ------------------------------------------------------------
3189    /// Decay strategy for `working` memories (default: `"exponential"`).
3190    #[serde(skip_serializing_if = "Option::is_none")]
3191    pub working_decay: Option<String>,
3192    /// Decay strategy for `episodic` memories (default: `"power_law"`).
3193    #[serde(skip_serializing_if = "Option::is_none")]
3194    pub episodic_decay: Option<String>,
3195    /// Decay strategy for `semantic` memories (default: `"logarithmic"`).
3196    #[serde(skip_serializing_if = "Option::is_none")]
3197    pub semantic_decay: Option<String>,
3198    /// Decay strategy for `procedural` memories (default: `"flat"` — no decay).
3199    #[serde(skip_serializing_if = "Option::is_none")]
3200    pub procedural_decay: Option<String>,
3201
3202    // Spaced repetition -------------------------------------------------------
3203    /// TTL extension multiplier per recall hit (default: 1.0; set to 0.0 to disable).
3204    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
3205    #[serde(skip_serializing_if = "Option::is_none")]
3206    pub spaced_repetition_factor: Option<f64>,
3207    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 d).
3208    #[serde(skip_serializing_if = "Option::is_none")]
3209    pub spaced_repetition_base_interval_seconds: Option<u64>,
3210
3211    // Proactive consolidation (COG-3) -----------------------------------------
3212    /// Enable background DBSCAN deduplication for this namespace (default: `false`).
3213    /// When `true` the server merges semantically near-duplicate memories every
3214    /// [`consolidation_interval_hours`](Self::consolidation_interval_hours) hours.
3215    #[serde(skip_serializing_if = "Option::is_none")]
3216    pub consolidation_enabled: Option<bool>,
3217    /// DBSCAN epsilon — cosine-similarity threshold to consider two memories
3218    /// duplicates (default: `0.92`; higher = only merge very close neighbours).
3219    #[serde(skip_serializing_if = "Option::is_none")]
3220    pub consolidation_threshold: Option<f32>,
3221    /// How often (in hours) the background consolidation job runs (default: `24`).
3222    #[serde(skip_serializing_if = "Option::is_none")]
3223    pub consolidation_interval_hours: Option<u32>,
3224    /// **Read-only.** Lifetime count of memories merged by the consolidation engine.
3225    /// The server manages this field; any value sent via [`set_memory_policy`] is ignored.
3226    ///
3227    /// [`set_memory_policy`]: crate::DakeraClient::set_memory_policy
3228    #[serde(skip_serializing_if = "Option::is_none")]
3229    pub consolidated_count: Option<u64>,
3230
3231    // Per-namespace rate limiting (SEC-5) -----------------------------------------
3232    /// Enable per-namespace store/recall rate limiting (default: `false`).
3233    #[serde(skip_serializing_if = "Option::is_none")]
3234    pub rate_limit_enabled: Option<bool>,
3235    /// Max store operations per minute for this namespace. `None` = unlimited (default).
3236    #[serde(skip_serializing_if = "Option::is_none")]
3237    pub rate_limit_stores_per_minute: Option<u32>,
3238    /// Max recall operations per minute for this namespace. `None` = unlimited (default).
3239    #[serde(skip_serializing_if = "Option::is_none")]
3240    pub rate_limit_recalls_per_minute: Option<u32>,
3241
3242    // Store-time deduplication (CE-10) -----------------------------------------
3243    /// Deduplicate against existing memories at store time (CE-10, default: `false`).
3244    ///
3245    /// When `true` the server computes a similarity check before persisting a new
3246    /// memory and drops it if a near-duplicate already exists (threshold controlled
3247    /// by [`dedup_threshold`](Self::dedup_threshold)).
3248    #[serde(skip_serializing_if = "Option::is_none")]
3249    pub dedup_on_store: Option<bool>,
3250    /// Cosine-similarity threshold for store-time deduplication (default: `0.92`).
3251    ///
3252    /// Memories with similarity ≥ this value are considered duplicates and the
3253    /// incoming memory is dropped. Only active when `dedup_on_store` is `true`.
3254    #[serde(skip_serializing_if = "Option::is_none")]
3255    pub dedup_threshold: Option<f32>,
3256}
3257
3258impl Default for MemoryPolicy {
3259    fn default() -> Self {
3260        Self {
3261            working_ttl_seconds: Some(14_400),
3262            episodic_ttl_seconds: Some(2_592_000),
3263            semantic_ttl_seconds: Some(31_536_000),
3264            procedural_ttl_seconds: Some(63_072_000),
3265            working_decay: Some("exponential".to_string()),
3266            episodic_decay: Some("power_law".to_string()),
3267            semantic_decay: Some("logarithmic".to_string()),
3268            procedural_decay: Some("flat".to_string()),
3269            spaced_repetition_factor: Some(1.0),
3270            spaced_repetition_base_interval_seconds: Some(86_400),
3271            consolidation_enabled: Some(false),
3272            consolidation_threshold: Some(0.92),
3273            consolidation_interval_hours: Some(24),
3274            consolidated_count: Some(0),
3275            rate_limit_enabled: Some(false),
3276            rate_limit_stores_per_minute: None,
3277            rate_limit_recalls_per_minute: None,
3278            dedup_on_store: Some(false),
3279            dedup_threshold: Some(0.92),
3280        }
3281    }
3282}
3283
3284// =============================================================================
3285// Engine Parity — Vector Bulk Ops, Agent Consolidation, Namespace Config
3286// =============================================================================
3287
3288/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-update`.
3289#[derive(Debug, Clone, Serialize, Deserialize)]
3290pub struct BulkUpdateRequest {
3291    pub filter: serde_json::Value,
3292    pub update: serde_json::Value,
3293}
3294
3295/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-update`.
3296#[derive(Debug, Clone, Serialize, Deserialize)]
3297pub struct BulkUpdateResponse {
3298    pub updated: u64,
3299    pub failed: u64,
3300    pub errors: Vec<String>,
3301}
3302
3303/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
3304#[derive(Debug, Clone, Serialize, Deserialize)]
3305pub struct BulkDeleteRequest {
3306    pub filter: serde_json::Value,
3307}
3308
3309/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
3310#[derive(Debug, Clone, Serialize, Deserialize)]
3311pub struct BulkDeleteResponse {
3312    pub deleted: u64,
3313    pub failed: u64,
3314    pub errors: Vec<String>,
3315}
3316
3317/// Request for `POST /v1/namespaces/{ns}/vectors/count`.
3318#[derive(Debug, Clone, Serialize, Deserialize)]
3319pub struct CountVectorsRequest {
3320    #[serde(skip_serializing_if = "Option::is_none")]
3321    pub filter: Option<serde_json::Value>,
3322}
3323
3324/// Response from `POST /v1/namespaces/{ns}/vectors/count`.
3325#[derive(Debug, Clone, Serialize, Deserialize)]
3326pub struct CountVectorsResponse {
3327    pub count: u64,
3328    pub namespace: String,
3329}
3330
3331/// Response from `POST /v1/agents/{agent_id}/consolidate`.
3332#[derive(Debug, Clone, Serialize, Deserialize)]
3333pub struct AgentConsolidateResponse {
3334    pub agent_id: String,
3335    pub memories_scanned: u64,
3336    pub clusters_found: u64,
3337    pub memories_deprecated: u64,
3338    pub anchor_ids: Vec<String>,
3339    pub deprecated_ids: Vec<String>,
3340    #[serde(skip_serializing_if = "Option::is_none")]
3341    pub skipped: Option<bool>,
3342    #[serde(skip_serializing_if = "Option::is_none")]
3343    pub reason: Option<String>,
3344}
3345
3346/// One entry in the agent consolidation log.
3347#[derive(Debug, Clone, Serialize, Deserialize)]
3348pub struct AgentConsolidationLogEntry {
3349    pub timestamp: u64,
3350    pub clusters_found: u64,
3351    pub memories_deprecated: u64,
3352    pub anchor_ids: Vec<String>,
3353    pub deprecated_ids: Vec<String>,
3354}
3355
3356/// Request for `PATCH /v1/agents/{agent_id}/consolidation/config`.
3357#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3358pub struct ConsolidationConfigPatch {
3359    #[serde(skip_serializing_if = "Option::is_none")]
3360    pub enabled: Option<bool>,
3361    #[serde(skip_serializing_if = "Option::is_none")]
3362    pub epsilon: Option<f64>,
3363    #[serde(skip_serializing_if = "Option::is_none")]
3364    pub min_samples: Option<u32>,
3365    #[serde(skip_serializing_if = "Option::is_none")]
3366    pub soft_deprecation_days: Option<u32>,
3367}
3368
3369/// Response from consolidation config endpoints.
3370#[derive(Debug, Clone, Serialize, Deserialize)]
3371pub struct AgentConsolidationConfig {
3372    pub enabled: bool,
3373    pub epsilon: f64,
3374    pub min_samples: u32,
3375    pub soft_deprecation_days: u32,
3376}
3377
3378/// Response from `GET /v1/namespaces/{ns}/config`.
3379#[derive(Debug, Clone, Serialize, Deserialize)]
3380pub struct NamespaceEntityConfig {
3381    pub namespace: String,
3382    pub extract_entities: bool,
3383    pub entity_types: Vec<String>,
3384}
3385
3386/// Response from `GET /v1/namespaces/{ns}/extractor`.
3387#[derive(Debug, Clone, Serialize, Deserialize)]
3388pub struct NamespaceExtractorConfig {
3389    pub provider: String,
3390    #[serde(skip_serializing_if = "Option::is_none")]
3391    pub model: Option<String>,
3392    #[serde(skip_serializing_if = "Option::is_none")]
3393    pub base_url: Option<String>,
3394}
3395
3396// ============================================================================
3397// Phase 2 Types — Cluster, Quotas, Backups, Ops
3398// ============================================================================
3399
3400/// Per-node replication lag entry.
3401#[derive(Debug, Clone, Serialize, Deserialize)]
3402pub struct NodeReplicationLag {
3403    pub node_id: String,
3404    pub lag_ms: u64,
3405    pub status: String,
3406}
3407
3408/// Response from `GET /v1/admin/cluster/replication`.
3409#[derive(Debug, Clone, Serialize, Deserialize)]
3410pub struct ReplicationStatus {
3411    pub replication_factor: u32,
3412    pub healthy_replicas: u32,
3413    pub total_nodes: u32,
3414    #[serde(default)]
3415    pub replication_lag: Vec<NodeReplicationLag>,
3416}
3417
3418/// Shard information.
3419#[derive(Debug, Clone, Serialize, Deserialize)]
3420pub struct ShardInfo {
3421    pub shard_id: String,
3422    pub namespace: String,
3423    pub primary_node: String,
3424    #[serde(default)]
3425    pub replica_nodes: Vec<String>,
3426    pub state: String,
3427    pub vector_count: u64,
3428    pub size_bytes: u64,
3429}
3430
3431/// Response from `GET /v1/admin/cluster/shards`.
3432#[derive(Debug, Clone, Serialize, Deserialize)]
3433pub struct ShardListResponse {
3434    pub shards: Vec<ShardInfo>,
3435    pub total: u32,
3436}
3437
3438/// Request for `POST /v1/admin/cluster/shards/rebalance`.
3439#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3440pub struct ShardRebalanceRequest {
3441    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3442    pub shard_ids: Vec<String>,
3443    #[serde(default)]
3444    pub dry_run: bool,
3445}
3446
3447/// A planned shard move.
3448#[derive(Debug, Clone, Serialize, Deserialize)]
3449pub struct ShardMove {
3450    pub shard_id: String,
3451    pub from_node: String,
3452    pub to_node: String,
3453}
3454
3455/// Response from `POST /v1/admin/cluster/shards/rebalance`.
3456#[derive(Debug, Clone, Serialize, Deserialize)]
3457pub struct ShardRebalanceResponse {
3458    pub initiated: bool,
3459    pub operation_id: String,
3460    pub shards_affected: u32,
3461    #[serde(skip_serializing_if = "Option::is_none")]
3462    pub estimated_seconds: Option<u64>,
3463    #[serde(default)]
3464    pub planned_moves: Vec<ShardMove>,
3465}
3466
3467/// Response from `GET /v1/admin/cluster/maintenance`.
3468#[derive(Debug, Clone, Serialize, Deserialize)]
3469pub struct MaintenanceStatus {
3470    pub enabled: bool,
3471    #[serde(skip_serializing_if = "Option::is_none")]
3472    pub reason: Option<String>,
3473    #[serde(skip_serializing_if = "Option::is_none")]
3474    pub enabled_at: Option<u64>,
3475    #[serde(skip_serializing_if = "Option::is_none")]
3476    pub scheduled_end: Option<u64>,
3477    #[serde(default)]
3478    pub nodes_in_maintenance: Vec<String>,
3479    pub rejecting_requests: bool,
3480}
3481
3482/// Request for `POST /v1/admin/cluster/maintenance/enable`.
3483#[derive(Debug, Clone, Serialize, Deserialize)]
3484pub struct EnableMaintenanceRequest {
3485    pub reason: String,
3486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3487    pub node_ids: Vec<String>,
3488    #[serde(default)]
3489    pub reject_requests: bool,
3490    #[serde(skip_serializing_if = "Option::is_none")]
3491    pub duration_minutes: Option<u32>,
3492}
3493
3494/// Request for `POST /v1/admin/cluster/maintenance/disable`.
3495#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3496pub struct DisableMaintenanceRequest {
3497    #[serde(skip_serializing_if = "Option::is_none")]
3498    pub force: Option<bool>,
3499}
3500
3501/// Quota configuration for a namespace.
3502#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3503pub struct QuotaConfig {
3504    #[serde(skip_serializing_if = "Option::is_none")]
3505    pub max_vectors: Option<u64>,
3506    #[serde(skip_serializing_if = "Option::is_none")]
3507    pub max_storage_bytes: Option<u64>,
3508    #[serde(skip_serializing_if = "Option::is_none")]
3509    pub max_dimensions: Option<usize>,
3510    #[serde(skip_serializing_if = "Option::is_none")]
3511    pub max_metadata_bytes: Option<usize>,
3512    #[serde(default)]
3513    pub enforcement: String,
3514}
3515
3516/// Quota usage for a namespace.
3517#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3518pub struct QuotaUsage {
3519    pub vector_count: u64,
3520    pub storage_bytes: u64,
3521    #[serde(skip_serializing_if = "Option::is_none")]
3522    pub avg_dimensions: Option<usize>,
3523    #[serde(skip_serializing_if = "Option::is_none")]
3524    pub avg_metadata_bytes: Option<usize>,
3525    pub last_updated: u64,
3526}
3527
3528/// Combined quota status.
3529#[derive(Debug, Clone, Serialize, Deserialize)]
3530pub struct QuotaStatus {
3531    pub namespace: String,
3532    pub config: QuotaConfig,
3533    pub usage: QuotaUsage,
3534    #[serde(skip_serializing_if = "Option::is_none")]
3535    pub vector_usage_percent: Option<f32>,
3536    #[serde(skip_serializing_if = "Option::is_none")]
3537    pub storage_usage_percent: Option<f32>,
3538    pub is_exceeded: bool,
3539    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3540    pub exceeded_quotas: Vec<String>,
3541}
3542
3543/// Response from `GET /v1/admin/quotas`.
3544#[derive(Debug, Clone, Serialize, Deserialize)]
3545pub struct QuotaListResponse {
3546    pub quotas: Vec<QuotaStatus>,
3547    pub total: u64,
3548    #[serde(skip_serializing_if = "Option::is_none")]
3549    pub default_config: Option<QuotaConfig>,
3550}
3551
3552/// Response from `GET /v1/admin/quotas/default`.
3553#[derive(Debug, Clone, Serialize, Deserialize)]
3554pub struct DefaultQuotaResponse {
3555    pub config: Option<QuotaConfig>,
3556}
3557
3558/// Request for `PUT /v1/admin/quotas/default`.
3559#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3560pub struct SetDefaultQuotaRequest {
3561    pub config: Option<QuotaConfig>,
3562}
3563
3564/// Request for `PUT /v1/admin/quotas/{namespace}`.
3565#[derive(Debug, Clone, Serialize, Deserialize)]
3566pub struct SetQuotaRequest {
3567    pub config: QuotaConfig,
3568}
3569
3570/// Response from `PUT /v1/admin/quotas`.
3571#[derive(Debug, Clone, Serialize, Deserialize)]
3572pub struct SetQuotaResponse {
3573    pub success: bool,
3574    pub namespace: String,
3575    pub config: QuotaConfig,
3576    pub message: String,
3577}
3578
3579/// Request for `POST /v1/admin/quotas/{namespace}/check`.
3580#[derive(Debug, Clone, Serialize, Deserialize)]
3581pub struct QuotaCheckRequest {
3582    pub vector_ids: Vec<String>,
3583    #[serde(skip_serializing_if = "Option::is_none")]
3584    pub dimensions: Option<usize>,
3585    #[serde(skip_serializing_if = "Option::is_none")]
3586    pub metadata_bytes: Option<usize>,
3587}
3588
3589/// Response from `POST /v1/admin/quotas/{namespace}/check`.
3590#[derive(Debug, Clone, Serialize, Deserialize)]
3591pub struct QuotaCheckResult {
3592    pub allowed: bool,
3593    #[serde(skip_serializing_if = "Option::is_none")]
3594    pub reason: Option<String>,
3595    pub usage: QuotaUsage,
3596    #[serde(skip_serializing_if = "Option::is_none")]
3597    pub exceeded_quota: Option<String>,
3598}
3599
3600/// Backup information.
3601#[derive(Debug, Clone, Serialize, Deserialize)]
3602pub struct AdminBackupInfo {
3603    pub backup_id: String,
3604    pub name: String,
3605    pub backup_type: String,
3606    pub status: String,
3607    #[serde(default)]
3608    pub namespaces: Vec<String>,
3609    pub vector_count: u64,
3610    pub size_bytes: u64,
3611    pub created_at: u64,
3612    #[serde(skip_serializing_if = "Option::is_none")]
3613    pub completed_at: Option<u64>,
3614    #[serde(skip_serializing_if = "Option::is_none")]
3615    pub duration_seconds: Option<u64>,
3616    #[serde(skip_serializing_if = "Option::is_none")]
3617    pub storage_path: Option<String>,
3618    #[serde(skip_serializing_if = "Option::is_none")]
3619    pub error: Option<String>,
3620    pub encrypted: bool,
3621    #[serde(skip_serializing_if = "Option::is_none")]
3622    pub compression: Option<String>,
3623}
3624
3625/// Response from `GET /v1/admin/backups`.
3626#[derive(Debug, Clone, Serialize, Deserialize)]
3627pub struct BackupListResponse {
3628    pub backups: Vec<AdminBackupInfo>,
3629    pub total: u64,
3630}
3631
3632/// Request for `POST /v1/admin/backups`.
3633#[derive(Debug, Clone, Serialize, Deserialize)]
3634pub struct CreateBackupRequest {
3635    pub name: String,
3636    #[serde(skip_serializing_if = "Option::is_none")]
3637    pub backup_type: Option<String>,
3638    #[serde(skip_serializing_if = "Option::is_none")]
3639    pub namespaces: Option<Vec<String>>,
3640    #[serde(skip_serializing_if = "Option::is_none")]
3641    pub encrypt: Option<bool>,
3642    #[serde(skip_serializing_if = "Option::is_none")]
3643    pub compression: Option<String>,
3644}
3645
3646/// Response from `POST /v1/admin/backups`.
3647#[derive(Debug, Clone, Serialize, Deserialize)]
3648pub struct CreateBackupResponse {
3649    pub backup: AdminBackupInfo,
3650    #[serde(skip_serializing_if = "Option::is_none")]
3651    pub estimated_completion: Option<u64>,
3652}
3653
3654/// Request for `POST /v1/admin/backups/restore`.
3655#[derive(Debug, Clone, Serialize, Deserialize)]
3656pub struct RestoreBackupRequest {
3657    pub backup_id: String,
3658    #[serde(skip_serializing_if = "Option::is_none")]
3659    pub target_namespaces: Option<Vec<String>>,
3660    #[serde(skip_serializing_if = "Option::is_none")]
3661    pub overwrite: Option<bool>,
3662    #[serde(skip_serializing_if = "Option::is_none")]
3663    pub point_in_time: Option<u64>,
3664}
3665
3666/// Response from `POST /v1/admin/backups/restore`.
3667#[derive(Debug, Clone, Serialize, Deserialize)]
3668pub struct RestoreBackupResponse {
3669    pub restore_id: String,
3670    pub status: String,
3671    pub backup_id: String,
3672    #[serde(default)]
3673    pub namespaces: Vec<String>,
3674    pub started_at: u64,
3675    #[serde(skip_serializing_if = "Option::is_none")]
3676    pub estimated_completion: Option<u64>,
3677    #[serde(skip_serializing_if = "Option::is_none")]
3678    pub progress_percent: Option<u8>,
3679    #[serde(skip_serializing_if = "Option::is_none")]
3680    pub vectors_restored: Option<u64>,
3681    #[serde(skip_serializing_if = "Option::is_none")]
3682    pub completed_at: Option<u64>,
3683    #[serde(skip_serializing_if = "Option::is_none")]
3684    pub duration_seconds: Option<u64>,
3685    #[serde(skip_serializing_if = "Option::is_none")]
3686    pub error: Option<String>,
3687}
3688
3689/// Backup schedule configuration.
3690#[derive(Debug, Clone, Serialize, Deserialize)]
3691pub struct BackupSchedule {
3692    pub enabled: bool,
3693    #[serde(skip_serializing_if = "Option::is_none")]
3694    pub cron: Option<String>,
3695    pub backup_type: String,
3696    pub retention_days: u32,
3697    pub max_backups: u32,
3698    #[serde(default)]
3699    pub namespaces: Vec<String>,
3700    pub encrypt: bool,
3701    #[serde(skip_serializing_if = "Option::is_none")]
3702    pub compression: Option<String>,
3703    #[serde(skip_serializing_if = "Option::is_none")]
3704    pub last_backup_at: Option<u64>,
3705    #[serde(skip_serializing_if = "Option::is_none")]
3706    pub next_backup_at: Option<u64>,
3707}
3708
3709/// Request for `POST /v1/admin/backups/schedule`.
3710#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3711pub struct UpdateBackupScheduleRequest {
3712    #[serde(skip_serializing_if = "Option::is_none")]
3713    pub enabled: Option<bool>,
3714    #[serde(skip_serializing_if = "Option::is_none")]
3715    pub cron: Option<String>,
3716    #[serde(skip_serializing_if = "Option::is_none")]
3717    pub backup_type: Option<String>,
3718    #[serde(skip_serializing_if = "Option::is_none")]
3719    pub retention_days: Option<u32>,
3720    #[serde(skip_serializing_if = "Option::is_none")]
3721    pub max_backups: Option<u32>,
3722    #[serde(skip_serializing_if = "Option::is_none")]
3723    pub namespaces: Option<Vec<String>>,
3724    #[serde(skip_serializing_if = "Option::is_none")]
3725    pub encrypt: Option<bool>,
3726    #[serde(skip_serializing_if = "Option::is_none")]
3727    pub compression: Option<String>,
3728}
3729
3730// ============================================================================
3731// Route Query (POST /v1/route)
3732// ============================================================================
3733
3734fn default_route_top_k() -> usize {
3735    3
3736}
3737
3738fn default_route_min_similarity() -> f32 {
3739    0.3
3740}
3741
3742/// Request for `POST /v1/route`.
3743#[derive(Debug, Clone, Serialize, Deserialize)]
3744pub struct RouteRequest {
3745    /// The query string to route.
3746    pub query: String,
3747    /// Maximum number of matching routes to return.
3748    #[serde(default = "default_route_top_k")]
3749    pub top_k: usize,
3750    /// Minimum similarity threshold for route matches.
3751    #[serde(default = "default_route_min_similarity")]
3752    pub min_similarity: f32,
3753    /// Optional embedding model override.
3754    #[serde(skip_serializing_if = "Option::is_none")]
3755    pub model: Option<String>,
3756}
3757
3758/// A single route match returned by `POST /v1/route`.
3759#[derive(Debug, Clone, Serialize, Deserialize)]
3760pub struct RouteMatch {
3761    /// Matched namespace name.
3762    pub namespace: String,
3763    /// Cosine similarity score.
3764    pub similarity: f64,
3765    /// Optional namespace description.
3766    #[serde(skip_serializing_if = "Option::is_none")]
3767    pub description: Option<String>,
3768}
3769
3770/// Response from `POST /v1/route`.
3771#[derive(Debug, Clone, Serialize, Deserialize)]
3772pub struct RouteResponse {
3773    /// Ordered list of route matches.
3774    pub routes: Vec<RouteMatch>,
3775    /// Embedding model used.
3776    pub model: String,
3777    /// Time spent computing embeddings (milliseconds).
3778    pub embedding_time_ms: u64,
3779}
3780
3781// ============================================================================
3782// Import Job Status (GET /v1/import/{job_id}/status)
3783// ============================================================================
3784
3785/// Status of an import job returned by `GET /v1/import/{job_id}/status`.
3786#[derive(Debug, Clone, Serialize, Deserialize)]
3787pub struct ImportJobStatus {
3788    /// Unique job identifier.
3789    pub job_id: String,
3790    /// Current job status (e.g. "running", "completed", "failed").
3791    pub status: String,
3792    /// Import file format (e.g. "jsonl", "csv").
3793    pub format: String,
3794    /// Total records in the import.
3795    pub total: usize,
3796    /// Records successfully imported.
3797    pub imported: usize,
3798    /// Records skipped (duplicates, invalid).
3799    pub skipped: usize,
3800    /// Per-record error messages, if any.
3801    #[serde(default)]
3802    pub errors: Vec<String>,
3803    /// Unix timestamp when the job started.
3804    pub started_at: u64,
3805    /// Unix timestamp when the job finished, if completed.
3806    #[serde(skip_serializing_if = "Option::is_none")]
3807    pub finished_at: Option<u64>,
3808}
3809
3810// ============================================================================
3811// Storage Tier Overview (GET /v1/admin/storage/tiers)
3812// ============================================================================
3813
3814/// Information about a single storage tier.
3815#[derive(Debug, Clone, Serialize, Deserialize)]
3816pub struct TierInfo {
3817    /// Tier name (e.g. "hot", "warm", "cold").
3818    pub name: String,
3819    /// Tier classification.
3820    pub tier_type: String,
3821    /// Underlying storage technology.
3822    pub technology: String,
3823    /// Human-readable tier description.
3824    pub description: String,
3825    /// Target access latency.
3826    pub target_latency: String,
3827    /// Optional capacity limit.
3828    #[serde(skip_serializing_if = "Option::is_none")]
3829    pub capacity: Option<String>,
3830    /// Current tier status.
3831    pub status: String,
3832    /// Number of items currently in this tier.
3833    pub current_count: u64,
3834    /// Number of cache/access hits.
3835    pub hit_count: u64,
3836    /// Hit rate as a fraction (0.0–1.0).
3837    pub hit_rate: f64,
3838}
3839
3840/// Tiered storage configuration parameters.
3841#[derive(Debug, Clone, Serialize, Deserialize)]
3842pub struct TierConfig {
3843    /// Maximum number of items in the hot tier.
3844    pub hot_tier_capacity: usize,
3845    /// Seconds of inactivity before promoting from hot to warm.
3846    pub hot_to_warm_threshold_secs: u64,
3847    /// Seconds of inactivity before demoting from warm to cold.
3848    pub warm_to_cold_threshold_secs: u64,
3849    /// Whether automatic tiering is enabled.
3850    pub auto_tier_enabled: bool,
3851    /// Interval between tier-check cycles (seconds).
3852    pub tier_check_interval_secs: u64,
3853}
3854
3855/// Tier movement activity counters.
3856#[derive(Debug, Clone, Serialize, Deserialize)]
3857pub struct TierActivity {
3858    /// Total promotions across all tiers.
3859    pub promotions: u64,
3860    /// Total demotions across all tiers.
3861    pub demotions: u64,
3862    /// Overall cache hit rate.
3863    pub cache_hit_rate: f64,
3864    /// Active storage backend name.
3865    pub storage_backend: String,
3866    /// Promotions specifically to the hot tier.
3867    pub promotions_to_hot: u64,
3868    /// Demotions specifically to warm.
3869    pub demotions_to_warm: u64,
3870    /// Demotions specifically to cold.
3871    pub demotions_to_cold: u64,
3872}
3873
3874/// Overview of the tiered storage system from `GET /v1/admin/storage/tiers`.
3875#[derive(Debug, Clone, Serialize, Deserialize)]
3876pub struct StorageTierOverview {
3877    /// Whether tiered storage is enabled.
3878    pub tiers_enabled: bool,
3879    /// Description of each tier.
3880    pub architecture: Vec<TierInfo>,
3881    /// Current tier configuration.
3882    pub config: TierConfig,
3883    /// Tier movement activity.
3884    pub activity: TierActivity,
3885}
3886
3887// ============================================================================
3888// Memory Type Stats (GET /v1/admin/memory-type-stats)
3889// ============================================================================
3890
3891/// Per-type memory statistics from `GET /v1/admin/memory-type-stats`.
3892#[derive(Debug, Clone, Serialize, Deserialize)]
3893pub struct MemoryTypeStatsResponse {
3894    /// Total number of memories.
3895    pub total: u64,
3896    /// Working memory count.
3897    pub working: u64,
3898    /// Episodic memory count.
3899    pub episodic: u64,
3900    /// Semantic memory count.
3901    pub semantic: u64,
3902    /// Procedural memory count.
3903    pub procedural: u64,
3904    /// Number of distinct agent namespaces.
3905    pub agent_namespaces: u64,
3906}
3907
3908// ============================================================================
3909// Migrate Namespace Dimensions (POST /v1/admin/namespaces/migrate-dimensions)
3910// ============================================================================
3911
3912fn default_target_dimension() -> usize {
3913    1024
3914}
3915
3916/// Request for `POST /v1/admin/namespaces/migrate-dimensions`.
3917#[derive(Debug, Clone, Serialize, Deserialize)]
3918pub struct MigrateNamespaceDimensionsRequest {
3919    /// Namespaces to migrate (empty = all).
3920    #[serde(default)]
3921    pub namespaces: Vec<String>,
3922    /// Target embedding dimension.
3923    #[serde(default = "default_target_dimension")]
3924    pub target_dimension: usize,
3925}
3926
3927/// Per-namespace migration result.
3928#[derive(Debug, Clone, Serialize, Deserialize)]
3929pub struct NamespaceMigrationResult {
3930    /// Namespace that was migrated.
3931    pub namespace: String,
3932    /// Original embedding dimension.
3933    pub original_dimension: usize,
3934    /// Vectors successfully re-embedded.
3935    pub vectors_migrated: usize,
3936    /// Vectors skipped (already at target dimension).
3937    pub vectors_skipped: usize,
3938    /// Migration status for this namespace.
3939    pub status: String,
3940    /// Error message if migration failed for this namespace.
3941    #[serde(skip_serializing_if = "Option::is_none")]
3942    pub error: Option<String>,
3943}
3944
3945/// Response from `POST /v1/admin/namespaces/migrate-dimensions`.
3946#[derive(Debug, Clone, Serialize, Deserialize)]
3947pub struct MigrateDimensionsResponse {
3948    /// Number of namespaces successfully migrated.
3949    pub migrated: usize,
3950    /// Number of namespaces that failed migration.
3951    pub failed: usize,
3952    /// Namespaces already at the target dimension.
3953    pub already_current: usize,
3954    /// Per-namespace results.
3955    pub results: Vec<NamespaceMigrationResult>,
3956}
3957
3958/// Request body for `POST /v1/admin/reembed/drain` (v0.11.82+). All fields optional.
3959#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3960pub struct DrainReembedRequest {
3961    /// Hard wall-clock cap in seconds (default 600).
3962    #[serde(skip_serializing_if = "Option::is_none")]
3963    pub timeout_secs: Option<u64>,
3964    /// Candidates upgraded per cycle (default 10000).
3965    #[serde(skip_serializing_if = "Option::is_none")]
3966    pub batch_size: Option<usize>,
3967    /// Minimum importance to upgrade (default 0.0 — upgrade all statics).
3968    #[serde(skip_serializing_if = "Option::is_none")]
3969    pub min_importance: Option<f32>,
3970}
3971
3972/// Response from `POST /v1/admin/reembed/drain` (v0.11.82+).
3973///
3974/// A [`remaining`][DrainReembedResponse::remaining] of `0` means all
3975/// `_embedding_kind=static` vectors have been upgraded to full ONNX quality.
3976#[derive(Debug, Clone, Serialize, Deserialize)]
3977pub struct DrainReembedResponse {
3978    /// Total vectors upgraded across all cycles in this drain.
3979    pub processed: usize,
3980    /// Static candidates still remaining (0 on a full drain).
3981    pub remaining: usize,
3982    /// Wall-clock duration of the drain in milliseconds.
3983    pub elapsed_ms: u128,
3984    /// Number of upgrade cycles executed.
3985    pub cycles: usize,
3986    /// `true` if the drain stopped on the timeout rather than reaching zero.
3987    pub timed_out: bool,
3988}
3989
3990/// Response from `GET /v1/admin/reembed/static-count` (v0.11.91+).
3991///
3992/// Returns the number of `_embedding_kind=static` vectors pending ONNX upgrade.
3993/// A [`static_count`][StaticCountResponse::static_count] of `0` means steady
3994/// state — all vectors are at full ONNX quality.
3995#[derive(Debug, Clone, Serialize, Deserialize)]
3996pub struct StaticCountResponse {
3997    /// Number of static vectors pending re-embedding.
3998    pub static_count: usize,
3999}
4000
4001#[cfg(test)]
4002mod embedding_model_tests {
4003    use super::*;
4004
4005    #[test]
4006    fn embedding_model_wire_values() {
4007        // Verify compound names serialize correctly — kebab-case rename_all would produce
4008        // "modern-bert-embed-base" (wrong); explicit #[serde(rename)] fixes these.
4009        assert_eq!(
4010            serde_json::to_string(&EmbeddingModel::ModernBertEmbedBase).unwrap(),
4011            r#""modernbert-embed-base""#
4012        );
4013        assert_eq!(
4014            serde_json::to_string(&EmbeddingModel::GteModernBertBase).unwrap(),
4015            r#""gte-modernbert-base""#
4016        );
4017        // Existing variants must not regress
4018        assert_eq!(
4019            serde_json::to_string(&EmbeddingModel::BgeLarge).unwrap(),
4020            r#""bge-large""#
4021        );
4022        assert_eq!(
4023            serde_json::to_string(&EmbeddingModel::E5Small).unwrap(),
4024            r#""e5-small""#
4025        );
4026    }
4027
4028    #[test]
4029    fn embedding_model_roundtrip() {
4030        for (variant, wire) in [
4031            (EmbeddingModel::ModernBertEmbedBase, "modernbert-embed-base"),
4032            (EmbeddingModel::GteModernBertBase, "gte-modernbert-base"),
4033        ] {
4034            let json = format!(r#""{wire}""#);
4035            let decoded: EmbeddingModel = serde_json::from_str(&json).unwrap();
4036            assert_eq!(decoded, variant);
4037        }
4038    }
4039}