use std::borrow::Cow;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use typed_builder::TypedBuilder;
use neuromance_common::chat::{Message, MessageRole};
use neuromance_common::client::{ChatRequest, Config, Usage};
use neuromance_common::tools::{FunctionCall, Tool, ToolCall};
pub mod client;
pub use client::{OpenAIClient, convert_chunk_to_chat_chunk};
#[derive(Debug, Deserialize)]
pub struct ChatChoice {
pub index: u32,
pub message: OpenAIMessage,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
pub struct OpenAIMessage {
pub role: MessageRole,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<SmallVec<[OpenAIToolCall; 2]>>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl From<&Message> for OpenAIMessage {
fn from(message: &Message) -> Self {
let tool_calls = if !message.tool_calls.is_empty() {
Some(
message
.tool_calls
.iter()
.map(OpenAIToolCall::from)
.collect(),
)
} else {
None
};
let content = if !message.content.is_empty() {
Some(message.content.clone())
} else {
None
};
OpenAIMessage::builder()
.role(message.role)
.content(content)
.name(message.name.clone())
.tool_calls(tool_calls)
.tool_call_id(message.tool_call_id.clone())
.build()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIToolCall {
pub id: Cow<'static, str>,
#[serde(rename = "type", default = "default_tool_call_type")]
pub r#type: Cow<'static, str>,
pub function: OpenAIFunction,
}
impl From<&ToolCall> for OpenAIToolCall {
fn from(tool_call: &ToolCall) -> Self {
Self {
id: Cow::Owned(tool_call.id.clone()),
r#type: Cow::Owned(tool_call.call_type.clone()),
function: OpenAIFunction::from(&tool_call.function),
}
}
}
fn default_tool_call_type() -> Cow<'static, str> {
Cow::Borrowed("function")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIFunction {
pub name: Cow<'static, str>,
pub arguments: Cow<'static, str>,
}
impl From<&FunctionCall> for OpenAIFunction {
fn from(function_call: &FunctionCall) -> Self {
Self {
name: Cow::Owned(function_call.name.clone()),
arguments: function_call
.arguments
.first()
.map(|s| Cow::Owned(s.clone()))
.unwrap_or(Cow::Borrowed("")),
}
}
}
#[derive(Debug, Clone, Serialize, TypedBuilder)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<OpenAIMessage>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<HashMap<String, f32>>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[builder(default = Some(false))]
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_thinking: Option<bool>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<serde_json::Value>,
}
impl From<(&ChatRequest, &Config)> for ChatCompletionRequest {
fn from((request, config): (&ChatRequest, &Config)) -> Self {
let openai_messages: Vec<OpenAIMessage> =
request.messages.iter().map(OpenAIMessage::from).collect();
let tools: Option<Vec<Tool>> = request.tools.clone();
ChatCompletionRequest::builder()
.model(
request
.model
.clone()
.unwrap_or_else(|| config.model.clone()),
)
.messages(openai_messages)
.max_tokens(request.max_tokens)
.temperature(request.temperature)
.top_p(request.top_p)
.stop(request.stop.clone())
.presence_penalty(request.presence_penalty)
.frequency_penalty(request.frequency_penalty)
.user(request.user.clone())
.stream(Some(request.stream))
.tools(tools)
.tool_choice(request.tool_choice.as_ref().map(|tc| tc.clone().into()))
.enable_thinking(request.enable_thinking)
.build()
}
}
impl ChatCompletionRequest {
pub fn with_logit_bias(mut self, logit_bias: HashMap<String, f32>) -> Self {
self.logit_bias = Some(logit_bias);
self
}
}
#[derive(Debug, Deserialize)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<ChatChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Debug, Deserialize)]
pub struct ChatStreamChoice {
pub index: u32,
pub delta: OpenAIMessageDelta,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIMessageDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<MessageRole>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<SmallVec<[OpenAIToolCallDelta; 2]>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIToolCallDelta {
pub index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<OpenAIFunctionDelta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIFunctionDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<ChatStreamChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}