use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::EmbedInfoResult;
pub const LOCAL_MODEL: &str = "Qwen/Qwen3-VL-Embedding-2B";
pub const MAX_TOKENS: usize = 8192;
pub const DEFAULT_DIMENSION: u32 = 1024;
pub const DEFAULT_MAX_TOKENS: usize = 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Slot {
Local,
Remote,
}
impl fmt::Display for Slot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Slot::Local => "local",
Slot::Remote => "remote",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LocalModel {
#[serde(default = "default_local_model")]
pub model: String,
#[serde(default = "default_dimension")]
pub dimension: u32,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
}
fn default_local_model() -> String {
LOCAL_MODEL.to_owned()
}
fn default_dimension() -> u32 {
DEFAULT_DIMENSION
}
fn default_max_tokens() -> usize {
DEFAULT_MAX_TOKENS
}
impl Default for LocalModel {
fn default() -> Self {
LocalModel {
model: default_local_model(),
dimension: DEFAULT_DIMENSION,
max_tokens: DEFAULT_MAX_TOKENS,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteModel {
pub endpoint: String,
pub model: String,
pub dimension: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ModelSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<LocalModel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote: Option<RemoteModel>,
}
impl ModelSettings {
pub fn is_empty(&self) -> bool {
self.local.is_none() && self.remote.is_none()
}
pub fn validate(&self) -> Result<(), String> {
if let Some(local) = &self.local {
if local.model != LOCAL_MODEL {
return Err(format!(
"local: only `{LOCAL_MODEL}` is supported, not `{}`",
local.model
));
}
if !(64..=2048).contains(&local.dimension) {
return Err(format!(
"local: `dimension` = {} is out of range (64..=2048)",
local.dimension
));
}
if !(1..=MAX_TOKENS).contains(&local.max_tokens) {
return Err(format!(
"local: `max_tokens` = {} is out of range (1..={MAX_TOKENS})",
local.max_tokens
));
}
}
if let Some(remote) = &self.remote {
if !(remote.endpoint.starts_with("http://") || remote.endpoint.starts_with("https://"))
{
return Err(format!(
"remote: `endpoint` = `{}` is not an http(s) URL",
remote.endpoint
));
}
if remote.model.trim().is_empty() {
return Err("remote: `model` must not be empty".to_owned());
}
if remote.dimension == 0 {
return Err("remote: `dimension` must be at least 1".to_owned());
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SlotModel {
Local(LocalModel),
Remote(RemoteModel),
}
impl SlotModel {
pub fn slot(&self) -> Slot {
match self {
SlotModel::Local(_) => Slot::Local,
SlotModel::Remote(_) => Slot::Remote,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct DeviceSettings {
#[serde(default)]
pub local_enabled: Option<bool>,
#[serde(default)]
pub remote_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_dir: Option<PathBuf>,
}
impl DeviceSettings {
pub fn enabled(&self, slot: Slot) -> Option<bool> {
match slot {
Slot::Local => self.local_enabled,
Slot::Remote => self.remote_enabled,
}
}
}
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(transparent)]
pub struct ApiKey(String);
impl ApiKey {
pub fn new(key: impl Into<String>) -> ApiKey {
ApiKey(key.into().trim().to_owned())
}
pub fn expose(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ApiKey(..)")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ModelSource {
Workgroup,
Device,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EmbedNote {
NotConfigured,
DisabledOnDevice,
NoAvx2,
KeyMissing,
Invalid(String),
}
impl fmt::Display for EmbedNote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EmbedNote::NotConfigured => f.write_str("no model is configured"),
EmbedNote::DisabledOnDevice => f.write_str("turned off on this device"),
EmbedNote::NoAvx2 => f.write_str("the local model needs AVX2, which this CPU lacks"),
EmbedNote::KeyMissing => f.write_str("no API key is set for the remote model"),
EmbedNote::Invalid(why) => write!(f, "invalid settings: {why}"),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct EmbedSettingsResult {
pub models: ModelSettings,
#[serde(default)]
pub source: Option<ModelSource>,
#[serde(default)]
pub shadowed_device_models: bool,
#[serde(default)]
pub device: DeviceSettings,
#[serde(default)]
pub local_enabled: bool,
#[serde(default)]
pub remote_enabled: bool,
#[serde(default)]
pub active: Option<Slot>,
#[serde(default)]
pub avx2: bool,
#[serde(default)]
pub key_set: bool,
#[serde(default)]
pub info: EmbedInfoResult,
}
impl EmbedSettingsResult {
pub fn enabled(&self, slot: Slot) -> bool {
match slot {
Slot::Local => self.local_enabled,
Slot::Remote => self.remote_enabled,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmbedModelSetParams {
pub slot: Slot,
#[serde(default)]
pub model: Option<SlotModel>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmbedDeviceSetParams {
pub slot: Slot,
#[serde(default)]
pub enabled: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EmbedKeySetParams {
pub key: ApiKey,
}
#[derive(Clone, Debug)]
pub enum EmbedRequest {
Show,
ModelSet(EmbedModelSetParams),
DeviceSet(EmbedDeviceSetParams),
KeySet(ApiKey),
KeyClear,
}
pub fn describe(s: &EmbedSettingsResult) -> Vec<String> {
let mut lines = Vec::new();
lines.push(match s.source {
Some(ModelSource::Workgroup) if s.shadowed_device_models => {
"models: the workgroup's (this device's own [models] is ignored)".to_owned()
}
Some(ModelSource::Workgroup) => "models: the workgroup's".to_owned(),
Some(ModelSource::Device) => "models: this device's own".to_owned(),
None => "models: none".to_owned(),
});
let switch = |slot: Slot| -> String {
let on = if s.enabled(slot) { "on" } else { "off" };
match s.device.enabled(slot) {
Some(_) => on.to_owned(),
None if slot == Slot::Local && !s.avx2 => format!("{on} (auto: no AVX2)"),
None => format!("{on} (auto)"),
}
};
lines.push(match &s.models.local {
Some(m) => format!(
"local: {} ({} dimensions, {} tokens) — this device: {}",
m.model,
m.dimension,
m.max_tokens,
switch(Slot::Local)
),
None => "local: not configured".to_owned(),
});
lines.push(match &s.models.remote {
Some(m) => format!(
"remote: {} at {} ({} dimensions) — this device: {}, key {}",
m.model,
m.endpoint,
m.dimension,
switch(Slot::Remote),
if s.key_set { "set" } else { "not set" }
),
None => format!(
"remote: not configured{}",
if s.key_set { " (a key is set)" } else { "" }
),
});
let state = match (&s.active, &s.info.note) {
(Some(slot), None) => format!(
"active: {slot}, {}",
if s.info.loaded {
"loaded"
} else {
"not loaded"
}
),
(Some(slot), Some(note)) => format!("active: {slot} — {note}"),
(None, Some(note)) => format!("active: none — {note}"),
(None, None) => "active: none".to_owned(),
};
lines.push(state);
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn remote() -> RemoteModel {
RemoteModel {
endpoint: "https://api.example.com".into(),
model: "m".into(),
dimension: 8,
}
}
#[test]
fn local_defaults_validate() {
let s = ModelSettings {
local: Some(LocalModel::default()),
remote: Some(remote()),
};
assert_eq!(s.validate(), Ok(()));
}
#[test]
fn validation_keeps_the_local_rules() {
let mut local = LocalModel {
dimension: 4096,
..LocalModel::default()
};
let check = |l: &LocalModel| {
ModelSettings {
local: Some(l.clone()),
remote: None,
}
.validate()
};
assert!(check(&local).is_err());
local.dimension = 1024;
local.max_tokens = MAX_TOKENS + 1;
assert!(check(&local).is_err());
local.max_tokens = MAX_TOKENS;
assert!(check(&local).is_ok());
local.model = "other".into();
assert!(check(&local).is_err());
}
#[test]
fn validation_checks_the_remote_slot() {
let check = |r: RemoteModel| {
ModelSettings {
local: None,
remote: Some(r),
}
.validate()
};
assert!(
check(RemoteModel {
endpoint: "api.example.com".into(),
..remote()
})
.is_err()
);
assert!(
check(RemoteModel {
model: " ".into(),
..remote()
})
.is_err()
);
assert!(
check(RemoteModel {
dimension: 0,
..remote()
})
.is_err()
);
}
#[test]
fn the_key_never_prints() {
let key = ApiKey::new("sk-secret\n");
assert_eq!(key.expose(), "sk-secret");
let params = EmbedKeySetParams { key };
let shown = format!("{params:?} {:?}", EmbedRequest::KeySet(params.key.clone()));
assert!(!shown.contains("sk-secret"), "{shown}");
assert_eq!(
serde_json::to_value(¶ms).unwrap(),
serde_json::json!({ "key": "sk-secret" })
);
}
#[test]
fn slot_models_are_tagged_by_slot() {
let v = serde_json::to_value(SlotModel::Remote(remote())).unwrap();
assert_eq!(v["remote"]["model"], "m");
let back: SlotModel = serde_json::from_value(v).unwrap();
assert_eq!(back.slot(), Slot::Remote);
}
#[test]
fn an_info_without_a_note_still_parses() {
let info: EmbedInfoResult =
serde_json::from_value(serde_json::json!({ "enabled": false })).unwrap();
assert_eq!(info.note, None);
}
#[test]
fn describe_names_the_source_switches_and_state() {
let s = EmbedSettingsResult {
models: ModelSettings {
local: Some(LocalModel::default()),
remote: Some(remote()),
},
source: Some(ModelSource::Workgroup),
local_enabled: false,
remote_enabled: true,
active: Some(Slot::Remote),
avx2: false,
key_set: false,
info: EmbedInfoResult {
enabled: true,
note: Some(EmbedNote::KeyMissing),
..EmbedInfoResult::default()
},
..EmbedSettingsResult::default()
};
let lines = describe(&s);
assert_eq!(lines[0], "models: the workgroup's");
assert!(
lines[1].ends_with("this device: off (auto: no AVX2)"),
"{}",
lines[1]
);
assert!(
lines[2].ends_with("this device: on (auto), key not set"),
"{}",
lines[2]
);
assert_eq!(
lines[3],
"active: remote — no API key is set for the remote model"
);
}
#[test]
fn describe_without_settings() {
let s = EmbedSettingsResult {
avx2: true,
info: EmbedInfoResult {
note: Some(EmbedNote::NotConfigured),
..EmbedInfoResult::default()
},
..EmbedSettingsResult::default()
};
assert_eq!(
describe(&s),
vec![
"models: none",
"local: not configured",
"remote: not configured",
"active: none — no model is configured",
]
);
}
}