use super::V0Provider;
use crate::core::providers::base::HttpErrorMapper;
use crate::core::providers::unified_provider::ProviderError;
const PROVIDER_NAME: &str = "v0";
use crate::core::types::{
chat::ChatMessage,
chat::ChatRequest,
message::MessageRole,
responses::{ChatChoice, ChatResponse, FinishReason, Usage},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0ChatRequest {
pub messages: Vec<V0Message>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<V0Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0Message {
pub role: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<V0ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: V0Function,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0Function {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: V0FunctionCall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0ChatResponse {
pub id: String,
pub object: String,
pub created: i64,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
pub choices: Vec<V0Choice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<V0Usage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0Choice {
pub index: i32,
pub message: V0Message,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct V0Usage {
pub prompt_tokens: i32,
pub completion_tokens: i32,
pub total_tokens: i32,
}
pub struct V0ChatHandler;
impl V0ChatHandler {
pub async fn handle_chat_completion(
provider: &V0Provider,
request: ChatRequest,
) -> Result<ChatResponse, ProviderError> {
let v0_request = Self::transform_request(request)?;
let v0_response = Self::send_request(provider, v0_request).await?;
let response = Self::transform_response(v0_response)?;
Ok(response)
}
fn transform_request(request: ChatRequest) -> Result<V0ChatRequest, ProviderError> {
let messages = request
.messages
.into_iter()
.map(|msg| V0Message {
role: msg.role.to_string(),
content: match msg.content {
Some(crate::core::types::message::MessageContent::Text(text)) => text,
Some(crate::core::types::message::MessageContent::Parts(_)) => {
String::new()
}
None => String::new(),
},
tool_calls: None, tool_call_id: None,
})
.collect();
let tools = request.tools.map(|tools| {
tools
.into_iter()
.map(|tool| V0Tool {
tool_type: "function".to_string(),
function: V0Function {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
},
})
.collect()
});
Ok(V0ChatRequest {
messages,
model: request.model,
stream: Some(request.stream),
tools,
tool_choice: request
.tool_choice
.map(|tc| serde_json::to_value(tc).unwrap_or_default()),
})
}
async fn send_request(
provider: &V0Provider,
request: V0ChatRequest,
) -> Result<V0ChatResponse, ProviderError> {
let url = provider.get_endpoint("chat/completions");
let headers = provider.create_headers();
let response = provider
.client
.post(&url)
.headers(headers)
.json(&request)
.send()
.await
.map_err(|e| ProviderError::network(PROVIDER_NAME, e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return match status.as_u16() {
401 => Err(ProviderError::authentication(
PROVIDER_NAME,
"Authentication failed",
)),
429 => Err(ProviderError::rate_limit(PROVIDER_NAME, None)),
404 => Err(ProviderError::model_not_found(PROVIDER_NAME, request.model)),
_ => Err(HttpErrorMapper::map_status_code(
PROVIDER_NAME,
status.as_u16(),
&format!("HTTP {}: {}", status, error_text),
)),
};
}
let v0_response: V0ChatResponse = response
.json()
.await
.map_err(|e| ProviderError::serialization(PROVIDER_NAME, e.to_string()))?;
Ok(v0_response)
}
fn transform_response(v0_response: V0ChatResponse) -> Result<ChatResponse, ProviderError> {
let choices = v0_response
.choices
.into_iter()
.map(|choice| ChatChoice {
index: choice.index as u32,
message: ChatMessage {
role: match choice.message.role.as_str() {
"system" => MessageRole::System,
"user" => MessageRole::User,
"assistant" => MessageRole::Assistant,
"tool" => MessageRole::Tool,
"function" => MessageRole::Function,
_ => MessageRole::Assistant, },
content: Some(crate::core::types::message::MessageContent::Text(
choice.message.content,
)),
thinking: None,
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
},
finish_reason: choice
.finish_reason
.and_then(|reason| match reason.as_str() {
"stop" => Some(FinishReason::Stop),
"length" => Some(FinishReason::Length),
"tool_calls" => Some(FinishReason::ToolCalls),
"content_filter" => Some(FinishReason::ContentFilter),
"function_call" => Some(FinishReason::FunctionCall),
_ => None,
}),
logprobs: None,
})
.collect();
let usage = v0_response.usage.map(|usage| Usage {
prompt_tokens: usage.prompt_tokens as u32,
completion_tokens: usage.completion_tokens as u32,
total_tokens: usage.total_tokens as u32,
prompt_tokens_details: None,
completion_tokens_details: None,
thinking_usage: None,
});
Ok(ChatResponse {
id: v0_response.id,
object: v0_response.object,
created: v0_response.created,
model: v0_response.model,
system_fingerprint: v0_response.system_fingerprint,
choices,
usage,
})
}
pub fn get_supported_parameters() -> Vec<&'static str> {
vec!["messages", "model", "stream", "tools", "tool_choice"]
}
pub fn validate_request(request: &ChatRequest) -> Result<(), ProviderError> {
if request.messages.is_empty() {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
"Messages cannot be empty",
));
}
if request.model.is_empty() {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
"Model is required",
));
}
for (i, message) in request.messages.iter().enumerate() {
if message.role.is_empty() {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
format!("Message {} role cannot be empty", i),
));
}
match &message.content {
Some(crate::core::types::message::MessageContent::Text(text)) => {
if text.is_empty() {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
format!("Message {} content cannot be empty", i),
));
}
}
Some(crate::core::types::message::MessageContent::Parts(array)) => {
if array.is_empty() {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
format!("Message {} content array cannot be empty", i),
));
}
}
None => {
if message.role == MessageRole::User {
return Err(ProviderError::invalid_request(
PROVIDER_NAME,
format!("Message {} content cannot be None for user role", i),
));
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{chat::ChatMessage, message::MessageContent, message::MessageRole};
#[test]
fn test_transform_request() {
let request = ChatRequest {
messages: vec![ChatMessage {
role: MessageRole::User,
content: Some(MessageContent::Text("Hello, world!".to_string())),
..Default::default()
}],
model: "v0-default".to_string(),
stream: false,
..Default::default()
};
let v0_request = V0ChatHandler::transform_request(request).unwrap();
assert_eq!(v0_request.model, "v0-default");
assert_eq!(v0_request.messages.len(), 1);
assert_eq!(v0_request.messages[0].role, "user");
assert_eq!(v0_request.messages[0].content, "Hello, world!");
}
#[test]
fn test_validate_request() {
let valid_request = ChatRequest {
messages: vec![ChatMessage {
role: MessageRole::User,
content: Some(MessageContent::Text("Hello".to_string())),
..Default::default()
}],
model: "v0-default".to_string(),
..Default::default()
};
assert!(V0ChatHandler::validate_request(&valid_request).is_ok());
let invalid_request = ChatRequest {
messages: vec![],
model: "".to_string(),
..Default::default()
};
assert!(V0ChatHandler::validate_request(&invalid_request).is_err());
}
#[test]
fn test_supported_parameters() {
let params = V0ChatHandler::get_supported_parameters();
assert!(params.contains(&"messages"));
assert!(params.contains(&"model"));
assert!(params.contains(&"stream"));
assert!(params.contains(&"tools"));
assert!(params.contains(&"tool_choice"));
}
}