candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
use tempfile::tempdir;

use super::*;
use crate::registry::Backend;
use crate::registry::BackendSupport;
use crate::registry::Catalog;
use crate::registry::Support;

/// The spec is an empty directory, so a refusal moved below `hub::resolve` or `loader::discover` surfaces as `MissingFile`.
#[test]
fn a_dtype_the_registry_refuses_is_refused_before_anything_is_fetched() {
  let dir = tempdir().unwrap();
  let spec = dir.path().to_str().unwrap();
  let descriptor = crate::registry::rerank::lookup("BAAI/bge-reranker-v2-m3").expect("registered");
  match TextCrossEncoder::builder(spec)
    .descriptor(descriptor)
    .device(Device::Cpu)
    .dtype(DType::F16)
    .build()
  {
    Err(err @ CanddingError::UnsupportedOnDevice { .. }) => {
      assert!(err.to_string().contains("accelerator-only"), "{err}");
      let CanddingError::UnsupportedOnDevice {
        model,
        backend,
        dtype,
      } = err
      else {
        unreachable!()
      };
      assert_eq!(
        (model.as_str(), backend, dtype),
        (spec, Backend::Cpu, DType::F16)
      );
    }
    Ok(_) => panic!("a CPU F16 request against a dtype the row refuses must be refused"),
    Err(other) => panic!("unexpected error: {other}"),
  }
}

#[test]
fn an_unregistered_id_is_refused_by_name() {
  match TextCrossEncoder::builder("org/definitely-not-registered").build() {
    Err(CanddingError::UnregisteredModel { id, catalog }) => {
      assert_eq!(id, "org/definitely-not-registered");
      assert_eq!(catalog, Catalog::Rerank);
    }
    Ok(_) => panic!("an unregistered id must be refused"),
    Err(other) => panic!("unexpected error: {other}"),
  }
}

#[test]
fn a_pair_entry_refuses_an_instruction_by_name() {
  let model = stub_model(Box::new(PanicsIfCalled), 512, 8);
  let empty: [&str; 0] = [];
  let refusals = [
    model
      .rerank_with_instruction("a", &["a b"], "judge it", None)
      .err(),
    model.rerank_with_instruction("a", &empty, "", None).err(),
    model
      .pair_token_count_with_instruction("a", &["a b"], "judge it")
      .err(),
  ];
  for refused in refusals {
    match refused {
      Some(err @ CanddingError::InstructionOnPairInput { .. }) => {
        let message = err.to_string();
        assert!(message.contains("stub/rerank"), "{message}");
        assert!(message.contains("instruction"), "{message}");
      }
      other => panic!("expected an instruction refusal, got {other:?}"),
    }
  }
  assert_eq!(model.pair_token_count("a", &["a b"]).unwrap(), [6]);
}

/// The dtype check reads the device's own cell: refused where only the CPU's refuses, and past the check, into the
/// empty directory, where only the accelerators' do.
#[test]
fn the_dtype_check_reads_the_device_s_own_cell() {
  let (v, u) = (Support::Verified, Support::Unsupported);
  for (f16, refused) in [
    (
      BackendSupport {
        cpu: u,
        metal: v,
        cuda: v,
      },
      true,
    ),
    (
      BackendSupport {
        cpu: v,
        metal: u,
        cuda: u,
      },
      false,
    ),
  ] {
    let dir = tempfile::tempdir().unwrap();
    let mut descriptor =
      crate::registry::rerank::lookup("BAAI/bge-reranker-v2-m3").expect("registered");
    descriptor.dtype_backends.f16 = f16;
    let err = TextCrossEncoder::builder(dir.path().to_str().unwrap())
      .descriptor(descriptor)
      .device(Device::Cpu)
      .dtype(DType::F16)
      .build()
      .err()
      .expect("an empty directory loads nothing");
    let on_the_cpu = matches!(
      err,
      CanddingError::UnsupportedOnDevice {
        backend: Backend::Cpu,
        dtype: DType::F16,
        ..
      }
    );
    assert_eq!(on_the_cpu, refused, "{f16:?}: {err}");
    assert!(
      !matches!(err, CanddingError::UnsupportedDtype { .. }),
      "{f16:?}: {err}"
    );
  }
}