feat: add internal ZCLAW kernel crates to git tracking

This commit is contained in:
iven
2026-03-22 09:26:36 +08:00
parent d72c0f7161
commit 58cd24f85b
36 changed files with 10298 additions and 0 deletions

View 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,
}