use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BackendType {
OpenAI,
ZhiPuAI,
MiniMax,
Moonshot,
Anthropic,
Mistral,
DeepSeek,
Qwen,
Groq,
Local,
Yi,
Gemini,
Baichuan,
StepFun,
XAI,
Xiaomi,
Ernie,
}
impl BackendType {
pub fn as_str(self) -> &'static str {
match self {
Self::OpenAI => "openai",
Self::ZhiPuAI => "zhipuai",
Self::MiniMax => "minimax",
Self::Moonshot => "moonshot",
Self::Anthropic => "anthropic",
Self::Mistral => "mistral",
Self::DeepSeek => "deepseek",
Self::Qwen => "qwen",
Self::Groq => "groq",
Self::Local => "local",
Self::Yi => "yi",
Self::Gemini => "gemini",
Self::Baichuan => "baichuan",
Self::StepFun => "stepfun",
Self::XAI => "xai",
Self::Xiaomi => "xiaomi",
Self::Ernie => "ernie",
}
}
}
impl fmt::Display for BackendType {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
impl MessageRole {
pub fn as_str(self) -> &'static str {
match self {
Self::System => "system",
Self::User => "user",
Self::Assistant => "assistant",
Self::Tool => "tool",
}
}
}
impl fmt::Display for MessageRole {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageContent {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_control: Option<serde_json::Value>,
},
ImageUrl {
url: String,
},
}
impl MessageContent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
cache_control: None,
}
}
pub fn text_with_cache_control(
text: impl Into<String>,
cache_control: serde_json::Value,
) -> Self {
Self::Text {
text: text.into(),
cache_control: Some(cache_control),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: MessageRole,
pub content: Vec<MessageContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
impl Message {
pub fn text(role: MessageRole, text: impl Into<String>) -> Self {
Self {
role,
content: vec![MessageContent::text(text)],
name: None,
tool_call_id: None,
tool_calls: Vec::new(),
reasoning_content: None,
}
}
pub fn text_content(&self) -> Option<String> {
let mut parts = Vec::new();
for content in &self.content {
if let MessageContent::Text { text, .. } = content {
parts.push(text.as_str());
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ChatRequestOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_completion_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_format: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_options: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modalities: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prediction: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
impl ChatRequestOptions {
pub fn has_openai_json_extensions(&self) -> bool {
self.max_completion_tokens.is_some()
|| self.top_p.is_some()
|| !self.stop.is_empty()
|| self.response_format.is_some()
|| self.stream_options.is_some()
|| self.audio.is_some()
|| self.frequency_penalty.is_some()
|| self.logit_bias.is_some()
|| self.logprobs.is_some()
|| self.metadata.is_some()
|| self.modalities.is_some()
|| self.n.is_some()
|| self.parallel_tool_calls.is_some()
|| self.prediction.is_some()
|| self.presence_penalty.is_some()
|| self.reasoning_effort.is_some()
|| self.seed.is_some()
|| self.service_tier.is_some()
|| self.store.is_some()
|| self.top_logprobs.is_some()
|| self.user.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(default)]
pub options: ChatRequestOptions,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ChatTool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<String>,
#[serde(default, skip_serializing_if = "is_empty_extra_body")]
pub extra_body: serde_json::Value,
}
impl ChatRequest {
pub fn new(model: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
messages,
options: ChatRequestOptions::default(),
tools: Vec::new(),
tool_choice: None,
extra_body: serde_json::Value::Null,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatTool {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<serde_json::Value>,
}
impl ChatTool {
pub fn function(
name: impl Into<String>,
description: impl Into<String>,
parameters: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
parameters,
cache_control: None,
}
}
pub fn with_cache_control(mut self, cache_control: serde_json::Value) -> Self {
self.cache_control = Some(cache_control);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_content: Option<serde_json::Value>,
}
impl ToolCall {
pub fn function(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
index: None,
extra_content: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatResponse {
pub id: String,
pub model: String,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<ChatUsage>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ChatStreamDelta {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub content: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub reasoning_content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<ChatUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_content: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "is_false")]
pub done: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<u32>,
}
fn is_false(value: &bool) -> bool {
!*value
}
fn is_empty_extra_body(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Null => true,
serde_json::Value::Object(object) => object.is_empty(),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbeddingResponse {
pub model: String,
pub data: Vec<EmbeddingData>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmbeddingData {
pub index: u32,
pub embedding: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RerankResponse {
pub results: Vec<RerankResult>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RerankResult {
pub index: usize,
pub relevance_score: f32,
}
#[derive(Debug, thiserror::Error)]
pub enum VvLlmError {
#[error("configuration error: {0}")]
Configuration(String),
#[error("model not found: backend={backend} model={model}")]
ModelNotFound { backend: String, model: String },
#[error("endpoint not found: {0}")]
EndpointNotFound(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("http error: {0}")]
Http(String),
#[error("provider error: {0}")]
Provider(String),
}