vv-llm 0.1.1

Rust implementation surface for vv-llm
Documentation
use crate::{BackendType, VvLlmError};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EndpointConfig {
    pub id: String,
    #[serde(default)]
    pub api_base: Option<String>,
    #[serde(default)]
    pub api_key: Option<String>,
    #[serde(default)]
    pub organization: Option<String>,
    #[serde(default)]
    pub endpoint_type: Option<String>,
    #[serde(default)]
    pub region: Option<String>,
    #[serde(default)]
    pub is_bedrock: Option<bool>,
    #[serde(default)]
    pub is_vertex: Option<bool>,
    #[serde(default)]
    pub credentials: Value,
    #[serde(default)]
    pub extra: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct BackendConfig {
    #[serde(default)]
    pub models: HashMap<String, ModelConfig>,
    #[serde(flatten)]
    pub extra: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelConfig {
    pub id: String,
    #[serde(default)]
    pub endpoints: Vec<EndpointBinding>,
    #[serde(default)]
    pub context_length: Option<u32>,
    #[serde(default)]
    pub max_output_tokens: Option<u32>,
    #[serde(default)]
    pub function_call_available: Option<bool>,
    #[serde(default)]
    pub response_format_available: Option<bool>,
    #[serde(default)]
    pub native_multimodal: Option<bool>,
    #[serde(default)]
    pub protocol: Option<String>,
    #[serde(default)]
    pub request_mapping: Option<Value>,
    #[serde(default)]
    pub response_mapping: Option<Value>,
    #[serde(flatten)]
    pub extra: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct LlmSettings {
    #[serde(rename = "VERSION", default)]
    pub version: Option<String>,
    #[serde(default)]
    pub endpoints: Vec<EndpointConfig>,
    #[serde(default)]
    pub backends: HashMap<String, BackendConfig>,
    #[serde(default)]
    pub embedding_backends: HashMap<String, BackendConfig>,
    #[serde(default)]
    pub rerank_backends: HashMap<String, BackendConfig>,
    #[serde(flatten)]
    pub extra: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedModelConfig {
    pub backend: String,
    pub model: ModelConfig,
    pub model_id: String,
    pub endpoint: EndpointConfig,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EndpointBinding {
    Id(String),
    Config {
        endpoint_id: String,
        #[serde(default)]
        model_id: Option<String>,
        #[serde(default)]
        enabled: Option<bool>,
        #[serde(flatten)]
        extra: HashMap<String, Value>,
    },
}

impl EndpointBinding {
    pub fn endpoint_id(&self) -> &str {
        match self {
            Self::Id(endpoint_id) => endpoint_id,
            Self::Config { endpoint_id, .. } => endpoint_id,
        }
    }

    pub fn model_id<'a>(&'a self, default_model_id: &'a str) -> &'a str {
        match self {
            Self::Id(_) => default_model_id,
            Self::Config { model_id, .. } => model_id.as_deref().unwrap_or(default_model_id),
        }
    }

    pub fn enabled(&self) -> bool {
        match self {
            Self::Id(_) => true,
            Self::Config { enabled, .. } => enabled.unwrap_or(true),
        }
    }
}

impl LlmSettings {
    pub fn from_json_str(raw: &str) -> Result<Self, VvLlmError> {
        Ok(serde_json::from_str(raw)?)
    }

    pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, VvLlmError> {
        let raw = fs::read_to_string(path.as_ref())
            .map_err(|error| VvLlmError::Configuration(error.to_string()))?;
        Self::from_json_str(&raw)
    }

    pub fn resolve_chat_model(
        &self,
        backend: BackendType,
        model_id: &str,
    ) -> Result<ResolvedModelConfig, VvLlmError> {
        self.resolve_model_in_map(&self.backends, backend.as_str(), model_id)
    }

    pub fn resolve_embedding_model(
        &self,
        backend: &str,
        model_id: &str,
    ) -> Result<ResolvedModelConfig, VvLlmError> {
        self.resolve_model_in_map(&self.embedding_backends, backend, model_id)
    }

    pub fn resolve_rerank_model(
        &self,
        backend: &str,
        model_id: &str,
    ) -> Result<ResolvedModelConfig, VvLlmError> {
        self.resolve_model_in_map(&self.rerank_backends, backend, model_id)
    }

    fn resolve_model_in_map(
        &self,
        map: &HashMap<String, BackendConfig>,
        backend: &str,
        model_id: &str,
    ) -> Result<ResolvedModelConfig, VvLlmError> {
        let backend_config = map.get(backend).ok_or_else(|| VvLlmError::ModelNotFound {
            backend: backend.to_string(),
            model: model_id.to_string(),
        })?;
        let model = backend_config
            .models
            .get(model_id)
            .or_else(|| {
                backend_config
                    .models
                    .values()
                    .find(|model| model.id == model_id)
            })
            .ok_or_else(|| VvLlmError::ModelNotFound {
                backend: backend.to_string(),
                model: model_id.to_string(),
            })?;
        let binding = model
            .endpoints
            .iter()
            .find(|binding| binding.enabled())
            .ok_or_else(|| {
                VvLlmError::Configuration(format!("model {model_id} has no enabled endpoints"))
            })?;
        let endpoint_id = binding.endpoint_id();
        let endpoint = self
            .endpoints
            .iter()
            .find(|endpoint| endpoint.id == endpoint_id)
            .ok_or_else(|| VvLlmError::EndpointNotFound(endpoint_id.to_string()))?;

        Ok(ResolvedModelConfig {
            backend: backend.to_string(),
            model: model.clone(),
            model_id: binding.model_id(&model.id).to_string(),
            endpoint: endpoint.clone(),
        })
    }
}