feat: add internal ZCLAW kernel crates to git tracking
This commit is contained in:
226
crates/zclaw-runtime/src/driver/anthropic.rs
Normal file
226
crates/zclaw-runtime/src/driver/anthropic.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
//! Anthropic Claude driver implementation
|
||||
|
||||
use async_trait::async_trait;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zclaw_types::{Result, ZclawError};
|
||||
|
||||
use super::{CompletionRequest, CompletionResponse, ContentBlock, LlmDriver, StopReason};
|
||||
|
||||
/// Anthropic API driver
|
||||
pub struct AnthropicDriver {
|
||||
client: Client,
|
||||
api_key: SecretString,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl AnthropicDriver {
|
||||
pub fn new(api_key: SecretString) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
base_url: "https://api.anthropic.com".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_base_url(api_key: SecretString, base_url: String) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmDriver for AnthropicDriver {
|
||||
fn provider(&self) -> &str {
|
||||
"anthropic"
|
||||
}
|
||||
|
||||
fn is_configured(&self) -> bool {
|
||||
!self.api_key.expose_secret().is_empty()
|
||||
}
|
||||
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||
let api_request = self.build_api_request(&request);
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/v1/messages", self.base_url))
|
||||
.header("x-api-key", self.api_key.expose_secret())
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("content-type", "application/json")
|
||||
.json(&api_request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ZclawError::LlmError(format!("HTTP request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(ZclawError::LlmError(format!("API error {}: {}", status, body)));
|
||||
}
|
||||
|
||||
let api_response: AnthropicResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ZclawError::LlmError(format!("Failed to parse response: {}", e)))?;
|
||||
|
||||
Ok(self.convert_response(api_response))
|
||||
}
|
||||
}
|
||||
|
||||
impl AnthropicDriver {
|
||||
fn build_api_request(&self, request: &CompletionRequest) -> AnthropicRequest {
|
||||
let messages: Vec<AnthropicMessage> = request.messages
|
||||
.iter()
|
||||
.filter_map(|msg| match msg {
|
||||
zclaw_types::Message::User { content } => Some(AnthropicMessage {
|
||||
role: "user".to_string(),
|
||||
content: vec!(ContentBlock::Text { text: content.clone() }),
|
||||
}),
|
||||
zclaw_types::Message::Assistant { content, thinking } => {
|
||||
let mut blocks = Vec::new();
|
||||
if let Some(think) = thinking {
|
||||
blocks.push(ContentBlock::Thinking { thinking: think.clone() });
|
||||
}
|
||||
blocks.push(ContentBlock::Text { text: content.clone() });
|
||||
Some(AnthropicMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: blocks,
|
||||
})
|
||||
}
|
||||
zclaw_types::Message::ToolUse { id, tool, input } => Some(AnthropicMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentBlock::ToolUse {
|
||||
id: id.clone(),
|
||||
name: tool.to_string(),
|
||||
input: input.clone(),
|
||||
}],
|
||||
}),
|
||||
zclaw_types::Message::ToolResult { tool_call_id: _, tool: _, output, is_error } => {
|
||||
let content = if *is_error {
|
||||
format!("Error: {}", output)
|
||||
} else {
|
||||
output.to_string()
|
||||
};
|
||||
Some(AnthropicMessage {
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentBlock::Text { text: content }],
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tools: Vec<AnthropicTool> = request.tools
|
||||
.iter()
|
||||
.map(|t| AnthropicTool {
|
||||
name: t.name.clone(),
|
||||
description: t.description.clone(),
|
||||
input_schema: t.input_schema.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
AnthropicRequest {
|
||||
model: request.model.clone(),
|
||||
max_tokens: request.max_tokens.unwrap_or(4096),
|
||||
system: request.system.clone(),
|
||||
messages,
|
||||
tools: if tools.is_empty() { None } else { Some(tools) },
|
||||
temperature: request.temperature,
|
||||
stop_sequences: if request.stop.is_empty() { None } else { Some(request.stop.clone()) },
|
||||
stream: request.stream,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_response(&self, api_response: AnthropicResponse) -> CompletionResponse {
|
||||
let content: Vec<ContentBlock> = api_response.content
|
||||
.into_iter()
|
||||
.map(|block| match block.block_type.as_str() {
|
||||
"text" => ContentBlock::Text { text: block.text.unwrap_or_default() },
|
||||
"thinking" => ContentBlock::Thinking { thinking: block.thinking.unwrap_or_default() },
|
||||
"tool_use" => ContentBlock::ToolUse {
|
||||
id: block.id.unwrap_or_default(),
|
||||
name: block.name.unwrap_or_default(),
|
||||
input: block.input.unwrap_or(serde_json::Value::Null),
|
||||
},
|
||||
_ => ContentBlock::Text { text: String::new() },
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stop_reason = match api_response.stop_reason.as_deref() {
|
||||
Some("end_turn") => StopReason::EndTurn,
|
||||
Some("max_tokens") => StopReason::MaxTokens,
|
||||
Some("stop_sequence") => StopReason::StopSequence,
|
||||
Some("tool_use") => StopReason::ToolUse,
|
||||
_ => StopReason::EndTurn,
|
||||
};
|
||||
|
||||
CompletionResponse {
|
||||
content,
|
||||
model: api_response.model,
|
||||
input_tokens: api_response.usage.input_tokens,
|
||||
output_tokens: api_response.usage.output_tokens,
|
||||
stop_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic API types
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnthropicRequest {
|
||||
model: String,
|
||||
max_tokens: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
system: Option<String>,
|
||||
messages: Vec<AnthropicMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<Vec<AnthropicTool>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop_sequences: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnthropicMessage {
|
||||
role: String,
|
||||
content: Vec<ContentBlock>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AnthropicTool {
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnthropicResponse {
|
||||
content: Vec<AnthropicContentBlock>,
|
||||
model: String,
|
||||
stop_reason: Option<String>,
|
||||
usage: AnthropicUsage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnthropicContentBlock {
|
||||
#[serde(rename = "type")]
|
||||
block_type: String,
|
||||
text: Option<String>,
|
||||
thinking: Option<String>,
|
||||
id: Option<String>,
|
||||
name: Option<String>,
|
||||
input: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnthropicUsage {
|
||||
input_tokens: u32,
|
||||
output_tokens: u32,
|
||||
}
|
||||
49
crates/zclaw-runtime/src/driver/gemini.rs
Normal file
49
crates/zclaw-runtime/src/driver/gemini.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Google Gemini driver implementation
|
||||
|
||||
use async_trait::async_trait;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use reqwest::Client;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use super::{CompletionRequest, CompletionResponse, ContentBlock, LlmDriver, StopReason};
|
||||
|
||||
/// Google Gemini driver
|
||||
pub struct GeminiDriver {
|
||||
client: Client,
|
||||
api_key: SecretString,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl GeminiDriver {
|
||||
pub fn new(api_key: SecretString) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
base_url: "https://generativelanguage.googleapis.com/v1beta".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmDriver for GeminiDriver {
|
||||
fn provider(&self) -> &str {
|
||||
"gemini"
|
||||
}
|
||||
|
||||
fn is_configured(&self) -> bool {
|
||||
!self.api_key.expose_secret().is_empty()
|
||||
}
|
||||
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||
// TODO: Implement actual API call
|
||||
Ok(CompletionResponse {
|
||||
content: vec![ContentBlock::Text {
|
||||
text: "Gemini driver not yet implemented".to_string(),
|
||||
}],
|
||||
model: request.model,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
stop_reason: StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
}
|
||||
59
crates/zclaw-runtime/src/driver/local.rs
Normal file
59
crates/zclaw-runtime/src/driver/local.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Local LLM driver (Ollama, LM Studio, vLLM, etc.)
|
||||
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use super::{CompletionRequest, CompletionResponse, ContentBlock, LlmDriver, StopReason};
|
||||
|
||||
/// Local LLM driver for Ollama, LM Studio, vLLM, etc.
|
||||
pub struct LocalDriver {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl LocalDriver {
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ollama() -> Self {
|
||||
Self::new("http://localhost:11434/v1")
|
||||
}
|
||||
|
||||
pub fn lm_studio() -> Self {
|
||||
Self::new("http://localhost:1234/v1")
|
||||
}
|
||||
|
||||
pub fn vllm() -> Self {
|
||||
Self::new("http://localhost:8000/v1")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmDriver for LocalDriver {
|
||||
fn provider(&self) -> &str {
|
||||
"local"
|
||||
}
|
||||
|
||||
fn is_configured(&self) -> bool {
|
||||
// Local drivers don't require API keys
|
||||
true
|
||||
}
|
||||
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||
// TODO: Implement actual API call (OpenAI-compatible)
|
||||
Ok(CompletionResponse {
|
||||
content: vec![ContentBlock::Text {
|
||||
text: "Local driver not yet implemented".to_string(),
|
||||
}],
|
||||
model: request.model,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
stop_reason: StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
}
|
||||
169
crates/zclaw-runtime/src/driver/mod.rs
Normal file
169
crates/zclaw-runtime/src/driver/mod.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
//! LLM Driver trait and implementations
|
||||
//!
|
||||
//! This module provides a unified interface for multiple LLM providers.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use secrecy::SecretString;
|
||||
use zclaw_types::Result;
|
||||
|
||||
mod anthropic;
|
||||
mod openai;
|
||||
mod gemini;
|
||||
mod local;
|
||||
|
||||
pub use anthropic::AnthropicDriver;
|
||||
pub use openai::OpenAiDriver;
|
||||
pub use gemini::GeminiDriver;
|
||||
pub use local::LocalDriver;
|
||||
|
||||
/// LLM Driver trait - unified interface for all providers
|
||||
#[async_trait]
|
||||
pub trait LlmDriver: Send + Sync {
|
||||
/// Get the provider name
|
||||
fn provider(&self) -> &str;
|
||||
|
||||
/// Send a completion request
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse>;
|
||||
|
||||
/// Check if the driver is properly configured
|
||||
fn is_configured(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Completion request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompletionRequest {
|
||||
/// Model identifier
|
||||
pub model: String,
|
||||
/// System prompt
|
||||
pub system: Option<String>,
|
||||
/// Conversation messages
|
||||
pub messages: Vec<zclaw_types::Message>,
|
||||
/// Available tools
|
||||
pub tools: Vec<ToolDefinition>,
|
||||
/// Maximum tokens to generate
|
||||
pub max_tokens: Option<u32>,
|
||||
/// Temperature (0.0 - 1.0)
|
||||
pub temperature: Option<f32>,
|
||||
/// Stop sequences
|
||||
pub stop: Vec<String>,
|
||||
/// Enable streaming
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
impl Default for CompletionRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: String::new(),
|
||||
system: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
max_tokens: Some(4096),
|
||||
temperature: Some(0.7),
|
||||
stop: Vec::new(),
|
||||
stream: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool definition for LLM
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ToolDefinition {
|
||||
pub fn new(name: impl Into<String>, description: impl Into<String>, schema: serde_json::Value) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
input_schema: schema,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Completion response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompletionResponse {
|
||||
/// Generated content blocks
|
||||
pub content: Vec<ContentBlock>,
|
||||
/// Model used
|
||||
pub model: String,
|
||||
/// Input tokens
|
||||
pub input_tokens: u32,
|
||||
/// Output tokens
|
||||
pub output_tokens: u32,
|
||||
/// Stop reason
|
||||
pub stop_reason: StopReason,
|
||||
}
|
||||
|
||||
/// Content block in response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ContentBlock {
|
||||
Text { text: String },
|
||||
Thinking { thinking: String },
|
||||
ToolUse { id: String, name: String, input: serde_json::Value },
|
||||
}
|
||||
|
||||
/// Stop reason
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StopReason {
|
||||
EndTurn,
|
||||
MaxTokens,
|
||||
StopSequence,
|
||||
ToolUse,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Driver configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DriverConfig {
|
||||
Anthropic { api_key: SecretString },
|
||||
OpenAi { api_key: SecretString, base_url: Option<String> },
|
||||
Gemini { api_key: SecretString },
|
||||
Local { base_url: String },
|
||||
}
|
||||
|
||||
impl DriverConfig {
|
||||
pub fn anthropic(api_key: impl Into<String>) -> Self {
|
||||
Self::Anthropic {
|
||||
api_key: SecretString::new(api_key.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openai(api_key: impl Into<String>) -> Self {
|
||||
Self::OpenAi {
|
||||
api_key: SecretString::new(api_key.into()),
|
||||
base_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openai_with_base(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
|
||||
Self::OpenAi {
|
||||
api_key: SecretString::new(api_key.into()),
|
||||
base_url: Some(base_url.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gemini(api_key: impl Into<String>) -> Self {
|
||||
Self::Gemini {
|
||||
api_key: SecretString::new(api_key.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ollama() -> Self {
|
||||
Self::Local {
|
||||
base_url: "http://localhost:11434".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local(base_url: impl Into<String>) -> Self {
|
||||
Self::Local {
|
||||
base_url: base_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
293
crates/zclaw-runtime/src/driver/openai.rs
Normal file
293
crates/zclaw-runtime/src/driver/openai.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
//! OpenAI-compatible driver implementation
|
||||
|
||||
use async_trait::async_trait;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zclaw_types::{Result, ZclawError};
|
||||
|
||||
use super::{CompletionRequest, CompletionResponse, ContentBlock, LlmDriver, StopReason, ToolDefinition};
|
||||
|
||||
/// OpenAI-compatible driver
|
||||
pub struct OpenAiDriver {
|
||||
client: Client,
|
||||
api_key: SecretString,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl OpenAiDriver {
|
||||
pub fn new(api_key: SecretString) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
base_url: "https://api.openai.com/v1".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_base_url(api_key: SecretString, base_url: String) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmDriver for OpenAiDriver {
|
||||
fn provider(&self) -> &str {
|
||||
"openai"
|
||||
}
|
||||
|
||||
fn is_configured(&self) -> bool {
|
||||
!self.api_key.expose_secret().is_empty()
|
||||
}
|
||||
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||
let api_request = self.build_api_request(&request);
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/chat/completions", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&api_request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ZclawError::LlmError(format!("HTTP request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(ZclawError::LlmError(format!("API error {}: {}", status, body)));
|
||||
}
|
||||
|
||||
let api_response: OpenAiResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ZclawError::LlmError(format!("Failed to parse response: {}", e)))?;
|
||||
|
||||
Ok(self.convert_response(api_response, request.model))
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenAiDriver {
|
||||
fn build_api_request(&self, request: &CompletionRequest) -> OpenAiRequest {
|
||||
let messages: Vec<OpenAiMessage> = request.messages
|
||||
.iter()
|
||||
.filter_map(|msg| match msg {
|
||||
zclaw_types::Message::User { content } => Some(OpenAiMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some(content.clone()),
|
||||
tool_calls: None,
|
||||
}),
|
||||
zclaw_types::Message::Assistant { content, thinking: _ } => Some(OpenAiMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: Some(content.clone()),
|
||||
tool_calls: None,
|
||||
}),
|
||||
zclaw_types::Message::System { content } => Some(OpenAiMessage {
|
||||
role: "system".to_string(),
|
||||
content: Some(content.clone()),
|
||||
tool_calls: None,
|
||||
}),
|
||||
zclaw_types::Message::ToolUse { id, tool, input } => Some(OpenAiMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: None,
|
||||
tool_calls: Some(vec![OpenAiToolCall {
|
||||
id: id.clone(),
|
||||
r#type: "function".to_string(),
|
||||
function: FunctionCall {
|
||||
name: tool.to_string(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
}]),
|
||||
}),
|
||||
zclaw_types::Message::ToolResult { tool_call_id, output, is_error, .. } => Some(OpenAiMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(if *is_error {
|
||||
format!("Error: {}", output)
|
||||
} else {
|
||||
output.to_string()
|
||||
}),
|
||||
tool_calls: None,
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Add system prompt if provided
|
||||
let mut messages = messages;
|
||||
if let Some(system) = &request.system {
|
||||
messages.insert(0, OpenAiMessage {
|
||||
role: "system".to_string(),
|
||||
content: Some(system.clone()),
|
||||
tool_calls: None,
|
||||
});
|
||||
}
|
||||
|
||||
let tools: Vec<OpenAiTool> = request.tools
|
||||
.iter()
|
||||
.map(|t| OpenAiTool {
|
||||
r#type: "function".to_string(),
|
||||
function: FunctionDef {
|
||||
name: t.name.clone(),
|
||||
description: t.description.clone(),
|
||||
parameters: t.input_schema.clone(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
OpenAiRequest {
|
||||
model: request.model.clone(),
|
||||
messages,
|
||||
max_tokens: request.max_tokens,
|
||||
temperature: request.temperature,
|
||||
stop: if request.stop.is_empty() { None } else { Some(request.stop.clone()) },
|
||||
stream: request.stream,
|
||||
tools: if tools.is_empty() { None } else { Some(tools) },
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_response(&self, api_response: OpenAiResponse, model: String) -> CompletionResponse {
|
||||
let choice = api_response.choices.first();
|
||||
|
||||
let (content, stop_reason) = match choice {
|
||||
Some(c) => {
|
||||
let blocks = if let Some(text) = &c.message.content {
|
||||
vec![ContentBlock::Text { text: text.clone() }]
|
||||
} else if let Some(tool_calls) = &c.message.tool_calls {
|
||||
tool_calls.iter().map(|tc| ContentBlock::ToolUse {
|
||||
id: tc.id.clone(),
|
||||
name: tc.function.name.clone(),
|
||||
input: serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::Value::Null),
|
||||
}).collect()
|
||||
} else {
|
||||
vec![ContentBlock::Text { text: String::new() }]
|
||||
};
|
||||
|
||||
let stop = match c.finish_reason.as_deref() {
|
||||
Some("stop") => StopReason::EndTurn,
|
||||
Some("length") => StopReason::MaxTokens,
|
||||
Some("tool_calls") => StopReason::ToolUse,
|
||||
_ => StopReason::EndTurn,
|
||||
};
|
||||
|
||||
(blocks, stop)
|
||||
}
|
||||
None => (vec![ContentBlock::Text { text: String::new() }], StopReason::EndTurn),
|
||||
};
|
||||
|
||||
let (input_tokens, output_tokens) = api_response.usage
|
||||
.map(|u| (u.prompt_tokens, u.completion_tokens))
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
CompletionResponse {
|
||||
content,
|
||||
model,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
stop_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI API types
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiRequest {
|
||||
model: String,
|
||||
messages: Vec<OpenAiMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stop: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
stream: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<Vec<OpenAiTool>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiMessage {
|
||||
role: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tool_calls: Option<Vec<OpenAiToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiToolCall {
|
||||
id: String,
|
||||
r#type: String,
|
||||
function: FunctionCall,
|
||||
}
|
||||
|
||||
impl Default for OpenAiToolCall {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
r#type: "function".to_string(),
|
||||
function: FunctionCall {
|
||||
name: String::new(),
|
||||
arguments: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FunctionCall {
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiTool {
|
||||
r#type: String,
|
||||
function: FunctionDef,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FunctionDef {
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiResponse {
|
||||
choices: Vec<OpenAiChoice>,
|
||||
usage: Option<OpenAiUsage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiChoice {
|
||||
message: OpenAiResponseMessage,
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiResponseMessage {
|
||||
content: Option<String>,
|
||||
tool_calls: Option<Vec<OpenAiToolCallResponse>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiToolCallResponse {
|
||||
id: String,
|
||||
function: FunctionCallResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FunctionCallResponse {
|
||||
name: String,
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAiUsage {
|
||||
prompt_tokens: u32,
|
||||
completion_tokens: u32,
|
||||
}
|
||||
Reference in New Issue
Block a user