diff --git a/.gitignore b/.gitignore index ea8c4bf..c0ed7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/logs \ No newline at end of file diff --git a/README.md b/README.md index c9d12a6..8ce204a 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ CCORP is designed to work seamlessly with Anthropic's Claude Code CLI: ```bash export ANTHROPIC_BASE_URL=http://localhost:3000 - export ANTHROPIC_AUTH_TOKEN="your_openrouter_api_key" + export ANTHROPIC_API_KEY="your_openrouter_api_key" ``` 3. Run Claude Code as normal: diff --git a/src/anthropic_to_openai.rs b/src/anthropic_to_openai.rs index 41cfa7f..295e087 100644 --- a/src/anthropic_to_openai.rs +++ b/src/anthropic_to_openai.rs @@ -18,10 +18,30 @@ pub fn format_anthropic_to_openai(req: AnthropicRequest, settings: &Config) -> O let mut openapi_messages = Vec::new(); if let Some(system) = req.system { - if let Some(system_str) = system.as_str() { + let system_text = if let Some(system_str) = system.as_str() { + // Handle string format: "system prompt text" + system_str.to_string() + } else if let Some(system_array) = system.as_array() { + // Handle array format: [{"type": "text", "text": "..."}] + system_array + .iter() + .filter_map(|block| { + if block["type"] == "text" { + block["text"].as_str().map(|s| s.to_string()) + } else { + None + } + }) + .collect::>() + .join("\n\n") + } else { + String::new() + }; + + if !system_text.is_empty() { openapi_messages.push(OpenAIMessage { role: "system".to_string(), - content: Some(system_str.to_string()), + content: Some(system_text), tool_calls: None, tool_call_id: None, }); diff --git a/src/main.rs b/src/main.rs index 8fed35f..2724787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,15 @@ async fn messages_handler( headers: HeaderMap, Json(payload): Json, ) -> impl IntoResponse { + if let Some(path) = state.logging_path.as_ref() { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let anthropic_request_path = format!("{path}/{timestamp}-anthropic-request.json"); + let anthropic_request_json = serde_json::to_string_pretty(&payload).unwrap(); + std::fs::write(anthropic_request_path, anthropic_request_json).expect("Failed to write anthropic request log"); + } let settings_guard = state.config.read().await; let openai_request = anthropic_to_openai::format_anthropic_to_openai(payload, &settings_guard); diff --git a/src/models.rs b/src/models.rs index d4bb84b..b898618 100644 --- a/src/models.rs +++ b/src/models.rs @@ -2,6 +2,16 @@ use serde::{Deserialize, Serialize}; // Anthropic API Structs +#[derive(Debug, Serialize, Deserialize)] +pub struct AnthropicUsage { + pub input_tokens: u32, + pub output_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_creation_input_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_read_input_tokens: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct AnthropicMessage { pub role: String, @@ -32,10 +42,19 @@ pub struct AnthropicResponse { pub stop_reason: String, pub stop_sequence: Option, pub model: String, + pub usage: AnthropicUsage, } // OpenAI API Structs +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAIUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_tokens: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct OpenAIMessage { pub role: String, @@ -78,6 +97,7 @@ pub struct OpenAIResponse { pub id: String, pub choices: Vec, pub model: String, + pub usage: OpenAIUsage, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/openai_to_anthropic.rs b/src/openai_to_anthropic.rs index 1fb9666..416ab62 100644 --- a/src/openai_to_anthropic.rs +++ b/src/openai_to_anthropic.rs @@ -1,4 +1,4 @@ -use crate::models::*; +use crate::models::{AnthropicResponse, AnthropicUsage, OpenAIResponse}; use serde_json::json; pub fn format_openai_to_anthropic(resp: OpenAIResponse) -> AnthropicResponse { @@ -32,5 +32,11 @@ pub fn format_openai_to_anthropic(resp: OpenAIResponse) -> AnthropicResponse { }, stop_sequence: None, model: resp.model, + usage: AnthropicUsage { + input_tokens: resp.usage.prompt_tokens, + output_tokens: resp.usage.completion_tokens, + cache_creation_input_tokens: None, + cache_read_input_tokens: None, + }, } }