1use std::future::Future;
44use std::pin::Pin;
45use std::sync::atomic::{AtomicU64, Ordering};
46
47pub use super::token_ledger::SharedTokenBudget;
48use super::token_ledger::{LedgerLimits, LedgerRefusal, TokenLedger};
49
50pub const EMBEDDING_MODULE_NAME: &str = "submilli:embedding";
53
54pub const DEFAULT_MAX_EXECUTION_EMBEDDING_TOKENS: u64 = 2_000_000;
56pub const DEFAULT_MAX_HELD_EMBEDDING_TOKENS: u64 = 200_000;
58pub const DEFAULT_MAX_EMBEDDING_REQUESTS: u64 = 1_000;
60pub const DEFAULT_MAX_TEXTS_PER_CALL: u64 = 128;
62pub const DEFAULT_MAX_BYTES_PER_CALL: u64 = 2 * 1024 * 1024;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct EmbeddingLimits {
71 pub per_execution_tokens: u64,
72 pub max_held_tokens: u64,
73 pub max_requests: u64,
74 pub max_texts_per_call: u64,
75 pub max_bytes_per_call: u64,
76}
77
78impl Default for EmbeddingLimits {
79 fn default() -> Self {
80 Self {
81 per_execution_tokens: DEFAULT_MAX_EXECUTION_EMBEDDING_TOKENS,
82 max_held_tokens: DEFAULT_MAX_HELD_EMBEDDING_TOKENS,
83 max_requests: DEFAULT_MAX_EMBEDDING_REQUESTS,
84 max_texts_per_call: DEFAULT_MAX_TEXTS_PER_CALL,
85 max_bytes_per_call: DEFAULT_MAX_BYTES_PER_CALL,
86 }
87 }
88}
89
90pub fn estimate_embedding_tokens(text: &str) -> u64 {
92 (text.len() as u64).div_ceil(3)
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
98pub enum Purpose {
99 Query,
100 Document,
101}
102
103impl Purpose {
104 pub fn as_str(&self) -> &'static str {
105 match self {
106 Self::Query => "query",
107 Self::Document => "document",
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum EmbeddingLimitKind {
115 PerExecutionTokens {
116 requested: u64,
117 limit: u64,
118 },
119 AllExecutionsTokens {
120 requested: u64,
121 limit: u64,
122 },
123 HeldTokens {
125 held: u64,
126 limit: u64,
127 },
128 Requests {
130 limit: u64,
131 },
132}
133
134impl std::fmt::Display for EmbeddingLimitKind {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 match self {
137 Self::PerExecutionTokens { requested, limit } => write!(
138 f,
139 "execution embedding token budget: {requested} tokens exceeds the {limit} this \
140 execution may spend — embed fewer or shorter texts, or split the work across \
141 executions; the operator raises it with `--max-execution-embedding-tokens`"
142 ),
143 Self::AllExecutionsTokens { requested, limit } => write!(
144 f,
145 "server embedding token budget: {requested} tokens exceeds the {limit} allowed \
146 across all live executions — this execution's own spend is not what is in the \
147 way; the operator raises the budget with `--max-embedding-tokens`"
148 ),
149 Self::HeldTokens { held, limit } => write!(
150 f,
151 "indeterminate embedding spend ceiling: {held} tokens are already held for requests \
152 whose usage the provider did not report, past the {limit} a single execution \
153 may hold — start a new execution to embed more"
154 ),
155 Self::Requests { limit } => write!(
156 f,
157 "embedding request limit: this execution already sent {limit} provider requests, \
158 the most it may send — embed more texts per call instead of calling more often; \
159 the operator raises it with `--max-execution-embedding-requests`"
160 ),
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum EmbeddingBoundKind {
168 TextCount,
169 TotalBytes,
170}
171
172impl std::fmt::Display for EmbeddingBoundKind {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Self::TextCount => write!(f, "texts in one call"),
176 Self::TotalBytes => write!(f, "bytes in one call"),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum EmbeddingFailureReason {
185 RateLimited,
186 RequestRejected,
187 ProviderUnavailable,
188 Transport,
189 Timeout,
190 BlockedByNetworkPolicy,
193}
194
195impl EmbeddingFailureReason {
196 pub fn as_str(&self) -> &'static str {
197 match self {
198 Self::RateLimited => "rate-limited",
199 Self::RequestRejected => "request-rejected",
200 Self::ProviderUnavailable => "provider-unavailable",
201 Self::Transport => "transport",
202 Self::Timeout => "timeout",
203 Self::BlockedByNetworkPolicy => "blocked-by-network-policy",
204 }
205 }
206
207 pub const ALL: [EmbeddingFailureReason; 6] = [
208 Self::RateLimited,
209 Self::RequestRejected,
210 Self::ProviderUnavailable,
211 Self::Transport,
212 Self::Timeout,
213 Self::BlockedByNetworkPolicy,
214 ];
215}
216
217impl std::fmt::Display for EmbeddingFailureReason {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.write_str(self.as_str())
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum EmbeddingMalformedReason {
226 CountMismatch,
227 DimensionMismatch,
228 NonFiniteValue,
229 InvalidBody,
230}
231
232impl EmbeddingMalformedReason {
233 pub fn as_str(&self) -> &'static str {
234 match self {
235 Self::CountMismatch => "count-mismatch",
236 Self::DimensionMismatch => "dimension-mismatch",
237 Self::NonFiniteValue => "non-finite-value",
238 Self::InvalidBody => "invalid-body",
239 }
240 }
241}
242
243impl std::fmt::Display for EmbeddingMalformedReason {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.write_str(self.as_str())
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
253pub struct SubBatchSettlement {
254 pub estimate: u64,
255 pub reported: u64,
256 pub indeterminate: u64,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
264pub enum EmbeddingError {
265 NotConfigured {
266 alias: String,
267 },
268 UnknownModel {
269 alias: String,
270 available: Vec<String>,
271 },
272 InputTooLong {
277 alias: String,
278 index: Option<usize>,
279 limit: Option<u64>,
280 settlements: Vec<SubBatchSettlement>,
281 },
282 BoundsExceeded {
283 alias: String,
284 kind: EmbeddingBoundKind,
285 actual: u64,
286 limit: u64,
287 },
288 BudgetExceeded {
291 alias: String,
292 limit_kind: EmbeddingLimitKind,
293 settlements: Vec<SubBatchSettlement>,
294 },
295 Unauthorized {
296 alias: String,
297 settlements: Vec<SubBatchSettlement>,
298 },
299 Provider {
300 alias: String,
301 reason: EmbeddingFailureReason,
302 settlements: Vec<SubBatchSettlement>,
303 },
304 Malformed {
305 alias: String,
306 reason: EmbeddingMalformedReason,
307 settlements: Vec<SubBatchSettlement>,
308 },
309 Internal {
312 alias: String,
313 settlements: Vec<SubBatchSettlement>,
314 },
315}
316
317impl EmbeddingError {
318 pub fn alias(&self) -> &str {
319 match self {
320 Self::NotConfigured { alias }
321 | Self::UnknownModel { alias, .. }
322 | Self::InputTooLong { alias, .. }
323 | Self::BoundsExceeded { alias, .. }
324 | Self::BudgetExceeded { alias, .. }
325 | Self::Unauthorized { alias, .. }
326 | Self::Provider { alias, .. }
327 | Self::Malformed { alias, .. }
328 | Self::Internal { alias, .. } => alias,
329 }
330 }
331
332 pub fn settlements(&self) -> &[SubBatchSettlement] {
335 match self {
336 Self::BudgetExceeded { settlements, .. }
337 | Self::Unauthorized { settlements, .. }
338 | Self::Provider { settlements, .. }
339 | Self::Malformed { settlements, .. }
340 | Self::Internal { settlements, .. }
341 | Self::InputTooLong { settlements, .. } => settlements,
342 Self::NotConfigured { .. }
343 | Self::UnknownModel { .. }
344 | Self::BoundsExceeded { .. } => &[],
345 }
346 }
347
348 pub fn is_budget_exceeded(&self) -> bool {
351 matches!(self, Self::BudgetExceeded { .. })
352 }
353
354 pub fn is_internal(&self) -> bool {
356 matches!(self, Self::Internal { .. })
357 }
358
359 pub fn with_settlements(mut self, collected: Vec<SubBatchSettlement>) -> Self {
362 match &mut self {
363 Self::BudgetExceeded { settlements, .. }
364 | Self::Unauthorized { settlements, .. }
365 | Self::Provider { settlements, .. }
366 | Self::Malformed { settlements, .. }
367 | Self::Internal { settlements, .. }
368 | Self::InputTooLong { settlements, .. } => *settlements = collected,
369 Self::NotConfigured { .. }
370 | Self::UnknownModel { .. }
371 | Self::BoundsExceeded { .. } => {}
372 }
373 self
374 }
375}
376
377impl std::fmt::Display for EmbeddingError {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379 match self {
380 Self::NotConfigured { alias } => write!(
381 f,
382 "embedding.embed(\"{alias}\"): no embedding provider is configured for this \
383 runtime — the operator wires one before embeddings can run"
384 ),
385 Self::UnknownModel { alias, available } => {
386 write!(
387 f,
388 "embedding.embed(\"{alias}\"): the provider does not serve that alias — "
389 )?;
390 if available.is_empty() {
391 write!(
392 f,
393 "it serves none, so declare an embedding model in the blueprint"
394 )
395 } else {
396 write!(
397 f,
398 "call embedding.models() and use one of: {}",
399 available.join(", ")
400 )
401 }
402 }
403 Self::InputTooLong {
404 alias,
405 index,
406 limit,
407 ..
408 } => {
409 write!(f, "embedding.embed(\"{alias}\"): ")?;
410 match (limit, index) {
411 (Some(limit), Some(index)) => write!(
412 f,
413 "text {index} exceeds the {limit} bytes this model accepts — shorten or \
414 split it"
415 ),
416 (Some(limit), None) => write!(
417 f,
418 "a text exceeds the {limit} bytes this model accepts — shorten or split it"
419 ),
420 (None, Some(index)) => write!(
421 f,
422 "the provider rejected text {index} as too long for this model — shorten \
423 or split it"
424 ),
425 (None, None) => write!(
426 f,
427 "the provider rejected a text as too long for this model — shorten or \
428 split it"
429 ),
430 }
431 }
432 Self::BoundsExceeded {
433 alias,
434 kind,
435 actual,
436 limit,
437 } => write!(
438 f,
439 "embedding.embed(\"{alias}\"): input bound exceeded: {actual} {kind} exceeds the \
440 {limit} allowed — send fewer or shorter texts per call"
441 ),
442 Self::BudgetExceeded {
443 alias, limit_kind, ..
444 } => write!(f, "embedding.embed(\"{alias}\") exceeded the {limit_kind}"),
445 Self::Unauthorized { alias, .. } => write!(
446 f,
447 "embedding.embed(\"{alias}\"): the provider rejected the API credential, or none \
448 was available — the operator sets a valid key for this provider"
449 ),
450 Self::Provider { alias, reason, .. } => write!(
451 f,
452 "embedding.embed(\"{alias}\"): the provider request failed: {reason}"
453 ),
454 Self::Malformed { alias, reason, .. } => write!(
455 f,
456 "embedding.embed(\"{alias}\"): the provider returned an unusable response: \
457 {reason}"
458 ),
459 Self::Internal { alias, .. } => {
460 write!(f, "embedding.embed(\"{alias}\"): internal host failure")
461 }
462 }
463 }
464}
465
466impl std::error::Error for EmbeddingError {}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum EmbeddingShapeError {
471 LengthMismatch {
473 len: usize,
474 count: usize,
475 dimensions: usize,
476 },
477 RowOutOfRange {
478 index: usize,
479 count: usize,
480 },
481}
482
483impl std::fmt::Display for EmbeddingShapeError {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 match self {
486 Self::LengthMismatch {
487 len,
488 count,
489 dimensions,
490 } => write!(
491 f,
492 "embedding batch holds {len} values but {count} rows of {dimensions} dimensions \
493 were declared"
494 ),
495 Self::RowOutOfRange { index, count } => {
496 write!(f, "embedding row {index} is out of range for {count} rows")
497 }
498 }
499 }
500}
501
502impl std::error::Error for EmbeddingShapeError {}
503
504#[derive(Debug, Clone, PartialEq, serde::Serialize)]
507pub struct EmbeddingBatch {
508 #[serde(with = "float_bytes")]
509 vectors: Vec<f32>,
510 count: usize,
511 dimensions: usize,
512 identity: String,
513 model: String,
514 input_tokens: Option<u64>,
515 settlements: Vec<SubBatchSettlement>,
516}
517
518impl<'de> serde::Deserialize<'de> for EmbeddingBatch {
521 fn deserialize<D: serde::Deserializer<'de>>(decoder: D) -> Result<Self, D::Error> {
522 #[derive(serde::Deserialize)]
523 struct Batch {
524 #[serde(with = "float_bytes")]
525 vectors: Vec<f32>,
526 count: usize,
527 dimensions: usize,
528 identity: String,
529 model: String,
530 input_tokens: Option<u64>,
531 settlements: Vec<SubBatchSettlement>,
532 }
533 let batch = Batch::deserialize(decoder)?;
534 EmbeddingBatch::new(
535 batch.vectors,
536 batch.count,
537 batch.dimensions,
538 batch.identity,
539 batch.model,
540 )
541 .map(|value| {
542 value
543 .with_input_tokens(batch.input_tokens)
544 .with_settlements(batch.settlements)
545 })
546 .map_err(serde::de::Error::custom)
547 }
548}
549
550impl EmbeddingBatch {
551 pub fn new(
553 vectors: Vec<f32>,
554 count: usize,
555 dimensions: usize,
556 identity: impl Into<String>,
557 model: impl Into<String>,
558 ) -> Result<Self, EmbeddingShapeError> {
559 let expected = count.checked_mul(dimensions);
560 if expected != Some(vectors.len()) {
561 return Err(EmbeddingShapeError::LengthMismatch {
562 len: vectors.len(),
563 count,
564 dimensions,
565 });
566 }
567 Ok(Self {
568 vectors,
569 count,
570 dimensions,
571 identity: identity.into(),
572 model: model.into(),
573 input_tokens: None,
574 settlements: Vec::new(),
575 })
576 }
577
578 pub fn with_input_tokens(mut self, input_tokens: Option<u64>) -> Self {
580 self.input_tokens = input_tokens;
581 self
582 }
583
584 pub fn with_settlements(mut self, settlements: Vec<SubBatchSettlement>) -> Self {
587 self.settlements = settlements;
588 self
589 }
590
591 pub fn count(&self) -> usize {
592 self.count
593 }
594
595 pub fn dimensions(&self) -> usize {
596 self.dimensions
597 }
598
599 pub fn identity(&self) -> &str {
600 &self.identity
601 }
602
603 pub fn model(&self) -> &str {
604 &self.model
605 }
606
607 pub fn input_tokens(&self) -> Option<u64> {
608 self.input_tokens
609 }
610
611 pub fn settlements(&self) -> &[SubBatchSettlement] {
612 &self.settlements
613 }
614
615 pub fn values(&self) -> &[f32] {
617 &self.vectors
618 }
619
620 pub fn row(&self, index: usize) -> Result<&[f32], EmbeddingShapeError> {
622 let out_of_range = EmbeddingShapeError::RowOutOfRange {
623 index,
624 count: self.count,
625 };
626 if index >= self.count {
627 return Err(out_of_range);
628 }
629 let start = index.checked_mul(self.dimensions).ok_or(out_of_range)?;
630 let end = start.checked_add(self.dimensions).ok_or(out_of_range)?;
631 self.vectors.get(start..end).ok_or(out_of_range)
632 }
633}
634
635#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
637pub struct EmbeddingModel {
638 pub name: String,
639 pub description: Option<String>,
640 pub dimensions: u64,
641 pub max_input_tokens: Option<u64>,
642 pub max_input_bytes: u64,
643 pub identity: String,
645}
646
647pub trait EmbeddingProvider: Send + Sync {
669 fn embed<'a>(
670 &'a self,
671 alias: &'a str,
672 texts: &'a [String],
673 purpose: Purpose,
674 budget: &'a EmbeddingTokenBudget,
675 ) -> Pin<Box<dyn Future<Output = Result<EmbeddingBatch, EmbeddingError>> + Send + 'a>>;
676
677 fn models<'a>(
679 &'a self,
680 ) -> Pin<Box<dyn Future<Output = Result<Vec<EmbeddingModel>, EmbeddingError>> + Send + 'a>>;
681
682 fn max_input_bytes(&self, alias: &str) -> Option<u64>;
685
686 fn estimate_tokens(&self, _alias: &str, texts: &[String]) -> u64 {
689 texts.iter().fold(0u64, |total, text| {
690 total.saturating_add(estimate_embedding_tokens(text))
691 })
692 }
693}
694
695pub struct EmbeddingTokenBudget {
698 limits: EmbeddingLimits,
699 ledger: TokenLedger,
700 requests: AtomicU64,
701}
702
703impl EmbeddingTokenBudget {
704 pub fn new(limits: EmbeddingLimits, aggregate: SharedTokenBudget) -> Self {
705 let ledger = TokenLedger::new(
706 LedgerLimits {
707 per_execution_tokens: limits.per_execution_tokens,
708 max_held_tokens: limits.max_held_tokens,
709 },
710 aggregate,
711 );
712 Self {
713 limits,
714 ledger,
715 requests: AtomicU64::new(0),
716 }
717 }
718
719 pub fn unmetered() -> Self {
721 Self::new(
722 EmbeddingLimits {
723 per_execution_tokens: u64::MAX,
724 max_held_tokens: u64::MAX,
725 max_requests: u64::MAX,
726 ..EmbeddingLimits::default()
727 },
728 SharedTokenBudget::new(u64::MAX),
729 )
730 }
731
732 pub fn limits(&self) -> EmbeddingLimits {
733 self.limits
734 }
735
736 pub fn used(&self) -> u64 {
737 self.ledger.used()
738 }
739
740 pub fn held(&self) -> u64 {
741 self.ledger.held()
742 }
743 pub fn restore_spend(&self, used: u64, held: u64, requests: u64) {
745 self.ledger.restore_spend(used, held);
746 let _ = self
747 .requests
748 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
749 Some(count.saturating_add(requests))
750 });
751 }
752
753 pub fn requests(&self) -> u64 {
755 self.requests.load(Ordering::Relaxed)
756 }
757
758 pub fn reserve(&self, alias: &str, tokens: u64) -> Result<(), EmbeddingError> {
762 let refused = |limit_kind| EmbeddingError::BudgetExceeded {
763 alias: alias.to_string(),
764 limit_kind,
765 settlements: Vec::new(),
766 };
767 self.ledger.reserve(tokens).map_err(|refusal| {
768 refused(match refusal {
769 LedgerRefusal::HeldReserve { held, limit } => {
770 EmbeddingLimitKind::HeldTokens { held, limit }
771 }
772 LedgerRefusal::PerExecution { requested, limit } => {
773 EmbeddingLimitKind::PerExecutionTokens { requested, limit }
774 }
775 LedgerRefusal::AllExecutions { requested, limit } => {
776 EmbeddingLimitKind::AllExecutionsTokens { requested, limit }
777 }
778 })
779 })
780 }
781
782 pub fn mark_sent(&self, alias: &str, estimate: u64) -> Result<(), EmbeddingError> {
787 let max = self.limits.max_requests;
788 let counted = self
789 .requests
790 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |sent| {
791 (sent < max).then(|| sent.saturating_add(1))
792 });
793 if counted.is_err() {
794 return Err(EmbeddingError::BudgetExceeded {
795 alias: alias.to_string(),
796 limit_kind: EmbeddingLimitKind::Requests { limit: max },
797 settlements: Vec::new(),
798 });
799 }
800 self.ledger.reconcile(estimate, 0, estimate);
801 Ok(())
802 }
803
804 pub fn settle(&self, reserved: u64, settlements: &[SubBatchSettlement]) {
807 let mut sent = 0u64;
808 for settlement in settlements {
809 self.ledger.settle_held(
810 settlement.estimate,
811 settlement.reported,
812 settlement.indeterminate,
813 );
814 sent = sent.saturating_add(settlement.estimate);
815 }
816 let unsent = reserved.saturating_sub(sent);
817 if unsent > 0 {
818 self.ledger.reconcile(unsent, 0, 0);
819 }
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826
827 const SAMPLE: &str = "SECRET-SAMPLE-INPUT-TEXT";
828
829 fn limits() -> EmbeddingLimits {
830 EmbeddingLimits {
831 per_execution_tokens: 1_000,
832 max_held_tokens: 500,
833 max_requests: 3,
834 ..EmbeddingLimits::default()
835 }
836 }
837
838 fn budget(aggregate: &SharedTokenBudget) -> EmbeddingTokenBudget {
839 EmbeddingTokenBudget::new(limits(), aggregate.clone())
840 }
841
842 fn settle_one(estimate: u64, reported: u64, indeterminate: u64) -> SubBatchSettlement {
843 SubBatchSettlement {
844 estimate,
845 reported,
846 indeterminate,
847 }
848 }
849
850 #[test]
851 fn default_limits_match_the_plan() {
852 let l = EmbeddingLimits::default();
853 assert_eq!(l.per_execution_tokens, 2_000_000);
854 assert_eq!(l.max_held_tokens, 200_000);
855 assert_eq!(l.max_requests, 1_000);
856 assert_eq!(l.max_texts_per_call, 128);
857 assert_eq!(l.max_bytes_per_call, 2 * 1024 * 1024);
858 }
859
860 #[test]
861 fn estimate_is_ceil_bytes_over_three() {
862 assert_eq!(estimate_embedding_tokens(""), 0);
863 assert_eq!(estimate_embedding_tokens("ab"), 1);
864 assert_eq!(estimate_embedding_tokens("abcd"), 2);
865 assert_eq!(estimate_embedding_tokens("é"), 1);
866 }
867
868 #[test]
869 fn reserve_over_the_per_run_cap_charges_nothing() {
870 let aggregate = SharedTokenBudget::new(10_000);
871 let b = EmbeddingTokenBudget::new(
872 EmbeddingLimits {
873 max_held_tokens: 5_000,
874 ..limits()
875 },
876 aggregate.clone(),
877 );
878 let err = b.reserve("m", 1_001).expect_err("over cap");
879 assert!(matches!(
880 err,
881 EmbeddingError::BudgetExceeded {
882 limit_kind: EmbeddingLimitKind::PerExecutionTokens {
883 requested: 1_001,
884 limit: 1_000
885 },
886 ..
887 }
888 ));
889 assert!(err.to_string().contains("--max-execution-embedding-tokens"));
890 assert_eq!((b.used(), aggregate.used()), (0, 0));
891 }
892
893 #[test]
894 fn aggregate_refusal_names_the_server_flag() {
895 let aggregate = SharedTokenBudget::new(10);
896 let b = budget(&aggregate);
897 let err = b.reserve("m", 11).expect_err("over aggregate");
898 assert!(err.to_string().contains("--max-embedding-tokens"));
899 assert_eq!((b.used(), aggregate.used()), (0, 0));
900 }
901
902 #[test]
903 fn reported_usage_below_estimate_refunds_the_difference() {
904 let aggregate = SharedTokenBudget::new(10_000);
905 let b = budget(&aggregate);
906 b.reserve("m", 300).expect("fits");
907 b.mark_sent("m", 300).expect("sent");
908 b.settle(300, &[settle_one(300, 120, 0)]);
909 assert_eq!((b.used(), b.held(), aggregate.used()), (120, 0, 120));
910 }
911
912 #[test]
913 fn reported_usage_above_the_estimate_is_charged() {
914 let aggregate = SharedTokenBudget::new(10_000);
915 let b = budget(&aggregate);
916 b.reserve("m", 1).expect("fits");
917 b.mark_sent("m", 1).expect("sent");
918 b.settle(1, &[settle_one(1, 7, 0)]);
919 assert_eq!((b.used(), b.held(), aggregate.used()), (7, 0, 7));
920 }
921
922 #[test]
923 fn no_usage_keeps_the_estimate_held_past_the_run() {
924 let aggregate = SharedTokenBudget::new(10_000);
925 {
926 let b = budget(&aggregate);
927 b.reserve("m", 300).expect("fits");
928 b.mark_sent("m", 300).expect("sent");
929 b.settle(300, &[settle_one(300, 0, 300)]);
930 assert_eq!((b.used(), b.held()), (300, 300));
931 }
932 assert_eq!(aggregate.used(), 300);
933 }
934
935 #[test]
936 fn failed_call_charges_reported_and_indeterminate_and_releases_the_rest() {
937 let aggregate = SharedTokenBudget::new(10_000);
938 let b = budget(&aggregate);
939 b.reserve("m", 400).expect("fits");
940 b.mark_sent("m", 100).expect("first sent");
941 b.mark_sent("m", 100).expect("second sent");
942 b.settle(400, &[settle_one(100, 40, 0), settle_one(100, 0, 100)]);
945 assert_eq!((b.used(), b.held(), aggregate.used()), (140, 100, 140));
946 }
947
948 #[test]
949 fn rejected_sub_batch_releases_everything() {
950 let aggregate = SharedTokenBudget::new(10_000);
951 let b = budget(&aggregate);
952 b.reserve("m", 100).expect("fits");
953 b.mark_sent("m", 100).expect("sent");
954 b.settle(100, &[settle_one(100, 0, 0)]);
955 assert_eq!((b.used(), b.held(), aggregate.used()), (0, 0, 0));
956 }
957
958 #[test]
959 fn sent_then_dropped_stays_held_and_unsent_is_returned() {
960 let aggregate = SharedTokenBudget::new(10_000);
961 {
962 let b = budget(&aggregate);
963 b.reserve("m", 300).expect("fits");
964 b.mark_sent("m", 100).expect("sent");
965 }
967 assert_eq!(aggregate.used(), 100);
968 }
969
970 #[test]
971 fn a_call_is_refused_once_held_reserve_is_past_the_cap() {
972 let aggregate = SharedTokenBudget::new(10_000);
973 let b = budget(&aggregate);
974 b.reserve("m", 600)
975 .expect("one call may exceed the held cap");
976 b.mark_sent("m", 600).expect("sent");
977 b.settle(600, &[settle_one(600, 0, 600)]);
978 let err = b.reserve("m", 1).expect_err("held is past the cap");
979 assert!(matches!(
980 err,
981 EmbeddingError::BudgetExceeded {
982 limit_kind: EmbeddingLimitKind::HeldTokens {
983 held: 600,
984 limit: 500
985 },
986 ..
987 }
988 ));
989 assert!(err.to_string().contains("held"));
990 assert_eq!(b.used(), 600);
991 }
992
993 #[test]
994 fn held_reserve_at_the_cap_still_admits_a_call() {
995 let aggregate = SharedTokenBudget::new(10_000);
996 let b = budget(&aggregate);
997 b.reserve("m", 500).expect("fits");
998 b.mark_sent("m", 500).expect("sent");
999 b.settle(500, &[settle_one(500, 0, 500)]);
1000 b.reserve("m", 100)
1001 .expect("held equals the cap, not past it");
1002 }
1003
1004 #[test]
1005 fn request_past_the_cap_is_refused_before_sent() {
1006 let aggregate = SharedTokenBudget::new(10_000);
1007 let b = budget(&aggregate);
1008 b.reserve("m", 100).expect("fits");
1009 for _ in 0..3 {
1010 b.mark_sent("m", 1).expect("within cap");
1011 }
1012 let held_before = b.held();
1013 let err = b.mark_sent("m", 1).expect_err("fourth refused");
1014 assert!(matches!(
1015 err,
1016 EmbeddingError::BudgetExceeded {
1017 limit_kind: EmbeddingLimitKind::Requests { limit: 3 },
1018 ..
1019 }
1020 ));
1021 assert!(
1022 err.to_string()
1023 .contains("--max-execution-embedding-requests")
1024 );
1025 assert_eq!(b.requests(), 3);
1026 assert_eq!(b.held(), held_before, "a refused request holds nothing");
1027 }
1028
1029 #[test]
1030 fn dropping_the_budget_returns_unspent_tokens() {
1031 let aggregate = SharedTokenBudget::new(10_000);
1032 {
1033 let b = budget(&aggregate);
1034 b.reserve("m", 250).expect("fits");
1035 assert_eq!(aggregate.used(), 250);
1036 }
1037 assert_eq!(aggregate.used(), 0);
1038 }
1039
1040 #[test]
1041 fn unmetered_budget_never_refuses() {
1042 let b = EmbeddingTokenBudget::unmetered();
1043 b.reserve("m", 5_000_000).expect("fits");
1044 for _ in 0..2_000 {
1045 b.mark_sent("m", 0).expect("uncapped");
1046 }
1047 }
1048
1049 #[test]
1050 fn batch_row_access_is_checked() {
1051 let batch = EmbeddingBatch::new(vec![1.0, 2.0, 3.0, 4.0], 2, 2, "id", "m").expect("shape");
1052 assert_eq!(batch.row(0), Ok(&[1.0, 2.0][..]));
1053 assert_eq!(batch.row(1), Ok(&[3.0, 4.0][..]));
1054 assert_eq!(
1055 batch.row(2),
1056 Err(EmbeddingShapeError::RowOutOfRange { index: 2, count: 2 })
1057 );
1058 assert!(batch.row(usize::MAX).is_err());
1059 }
1060
1061 #[test]
1062 fn batch_construction_validates_length_with_checked_mul() {
1063 assert!(EmbeddingBatch::new(vec![1.0; 3], 2, 2, "id", "m").is_err());
1064 assert!(EmbeddingBatch::new(vec![], usize::MAX, 2, "id", "m").is_err());
1065 assert!(EmbeddingBatch::new(vec![], 0, 5, "id", "m").is_ok());
1066 }
1067
1068 #[test]
1069 fn no_error_display_contains_input_text() {
1070 let settlements = vec![settle_one(1, 0, 1)];
1071 let errors = vec![
1072 EmbeddingError::NotConfigured { alias: "m".into() },
1073 EmbeddingError::UnknownModel {
1074 alias: "m".into(),
1075 available: vec!["a".into()],
1076 },
1077 EmbeddingError::InputTooLong {
1078 alias: "m".into(),
1079 index: Some(2),
1080 limit: Some(9),
1081 settlements: Vec::new(),
1082 },
1083 EmbeddingError::InputTooLong {
1084 alias: "m".into(),
1085 index: Some(2),
1086 limit: None,
1087 settlements: Vec::new(),
1088 },
1089 EmbeddingError::BoundsExceeded {
1090 alias: "m".into(),
1091 kind: EmbeddingBoundKind::TextCount,
1092 actual: 200,
1093 limit: 128,
1094 },
1095 EmbeddingError::BudgetExceeded {
1096 alias: "m".into(),
1097 limit_kind: EmbeddingLimitKind::Requests { limit: 1 },
1098 settlements: settlements.clone(),
1099 },
1100 EmbeddingError::Unauthorized {
1101 alias: "m".into(),
1102 settlements: settlements.clone(),
1103 },
1104 EmbeddingError::Provider {
1105 alias: "m".into(),
1106 reason: EmbeddingFailureReason::Transport,
1107 settlements: settlements.clone(),
1108 },
1109 EmbeddingError::Malformed {
1110 alias: "m".into(),
1111 reason: EmbeddingMalformedReason::CountMismatch,
1112 settlements: settlements.clone(),
1113 },
1114 EmbeddingError::Internal {
1115 alias: "m".into(),
1116 settlements,
1117 },
1118 ];
1119 for error in errors {
1120 let text = error.to_string();
1121 assert!(!text.contains(SAMPLE), "{text}");
1122 assert!(text.contains("\"m\""), "{text}");
1123 }
1124 }
1125
1126 #[test]
1127 fn a_provider_side_length_rejection_does_not_claim_a_byte_limit() {
1128 let provider_side = EmbeddingError::InputTooLong {
1129 alias: "m".into(),
1130 index: Some(1),
1131 limit: None,
1132 settlements: Vec::new(),
1133 }
1134 .to_string();
1135 assert!(
1136 provider_side.contains("the provider rejected text 1 as too long"),
1137 "{provider_side}"
1138 );
1139 assert!(!provider_side.contains("bytes"), "{provider_side}");
1140 let pre_send = EmbeddingError::InputTooLong {
1141 alias: "m".into(),
1142 index: Some(1),
1143 limit: Some(9),
1144 settlements: Vec::new(),
1145 }
1146 .to_string();
1147 assert!(pre_send.contains("exceeds the 9 bytes"), "{pre_send}");
1148 }
1149
1150 #[test]
1151 fn post_send_errors_carry_settlements() {
1152 let err = EmbeddingError::Internal {
1153 alias: "m".into(),
1154 settlements: Vec::new(),
1155 }
1156 .with_settlements(vec![settle_one(5, 0, 5)]);
1157 assert_eq!(err.settlements(), &[settle_one(5, 0, 5)]);
1158 let pre = EmbeddingError::NotConfigured { alias: "m".into() }
1159 .with_settlements(vec![settle_one(5, 0, 5)]);
1160 assert!(pre.settlements().is_empty());
1161 }
1162
1163 #[test]
1164 fn failure_reason_spellings_are_stable_kebab_case() {
1165 let spelled: Vec<&str> = EmbeddingFailureReason::ALL
1166 .iter()
1167 .map(EmbeddingFailureReason::as_str)
1168 .collect();
1169 assert_eq!(
1170 spelled,
1171 [
1172 "rate-limited",
1173 "request-rejected",
1174 "provider-unavailable",
1175 "transport",
1176 "timeout",
1177 "blocked-by-network-policy"
1178 ]
1179 );
1180 }
1181}
1182
1183mod float_bytes {
1185 use serde::{Deserializer, Serializer};
1186 pub fn serialize<S: Serializer>(values: &[f32], serializer: S) -> Result<S::Ok, S::Error> {
1187 let bytes: Vec<_> = values
1188 .iter()
1189 .flat_map(|value| value.to_bits().to_le_bytes())
1190 .collect();
1191 serializer.serialize_bytes(&bytes)
1192 }
1193 pub fn deserialize<'de, D: Deserializer<'de>>(decoder: D) -> Result<Vec<f32>, D::Error> {
1194 struct Floats;
1195 impl<'de> serde::de::Visitor<'de> for Floats {
1196 type Value = Vec<f32>;
1197 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1198 f.write_str("binary float vector")
1199 }
1200 fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
1201 if !bytes.len().is_multiple_of(4) {
1202 return Err(E::custom("misaligned float vector"));
1203 }
1204 let mut values = Vec::new();
1205 values
1206 .try_reserve_exact(bytes.len() / 4)
1207 .map_err(E::custom)?;
1208 for word in bytes.as_chunks::<4>().0 {
1209 values.push(f32::from_bits(u32::from_le_bytes(*word)));
1210 }
1211 Ok(values)
1212 }
1213 }
1214 decoder.deserialize_bytes(Floats)
1215 }
1216
1217 #[cfg(test)]
1218 mod tests {
1219 #[derive(serde::Serialize, serde::Deserialize)]
1220 struct Floats(#[serde(with = "super")] Vec<f32>);
1221
1222 #[test]
1223 fn replay_vectors_preserve_bits_without_geometric_capacity_growth() {
1224 let mut values = vec![1.0; 65_537];
1225 values[0] = -0.0;
1226 values[1] = f32::from_bits(0x7fc0_1234);
1227 let encoded = rmp_serde::to_vec(&Floats(values.clone())).unwrap();
1228 let restored: Floats = rmp_serde::from_slice(&encoded).unwrap();
1229 assert_eq!(restored.0.capacity(), values.len());
1230 assert!(
1231 restored
1232 .0
1233 .iter()
1234 .zip(values)
1235 .all(|(a, b)| a.to_bits() == b.to_bits())
1236 );
1237 assert!(rmp_serde::from_slice::<Floats>(&[0xc4, 3, 0, 0, 0]).is_err());
1238 }
1239 }
1240}