├── migrations └── .gitkeep ├── claude-code-api ├── src │ ├── middleware │ │ ├── mod.rs │ │ ├── request_id.rs │ │ └── error_handler.rs │ ├── models │ │ ├── mod.rs │ │ ├── tests.rs │ │ ├── error.rs │ │ └── claude.rs │ ├── utils │ │ ├── mod.rs │ │ ├── streaming.rs │ │ └── parser.rs │ ├── api │ │ ├── mod.rs │ │ ├── sessions.rs │ │ ├── projects.rs │ │ ├── models.rs │ │ ├── stats.rs │ │ └── conversations.rs │ ├── core │ │ ├── mod.rs │ │ ├── database.rs │ │ ├── session_manager.rs │ │ ├── auth.rs │ │ └── config.rs │ └── bin │ │ └── ccapi.rs └── Cargo.toml ├── script ├── run.sh ├── start_with_permissions.sh ├── start_with_mcp.sh ├── test_mcp.sh ├── check_config.sh └── test_multimodal.sh ├── claude-code-sdk-rs ├── .gitignore ├── examples │ ├── .env.example │ ├── claude-settings.json │ ├── test_new_options.rs │ ├── custom-claude-settings.json │ ├── test_paths.rs │ ├── init_test.rs │ ├── test_client.rs │ ├── interactive_real_test.rs │ ├── simple_real_test.rs │ ├── test_settings.rs │ ├── test_process_leak.rs │ ├── README_MODELS.md │ ├── interactive_demo.rs │ ├── simple_query.rs │ ├── basic_client.rs │ ├── account_info.rs │ ├── query_with_file_ops.rs │ ├── test_control_reception.rs │ ├── test_settings_safe.rs │ ├── test_add_dirs.rs │ ├── demo_project_naming.rs │ ├── token_efficient.rs │ ├── token_budget_monitoring.rs │ ├── performance_comparison.rs │ ├── test_concurrent_leak.rs │ ├── simple_model_test.rs │ ├── session_with_account_info.rs │ ├── interactive.rs │ ├── check_current_account.rs │ ├── demo_new_features.rs │ ├── control_format_demo.rs │ ├── test_auto_download.rs │ ├── permission_modes.rs │ ├── code_generator.rs │ └── test_combined_features.rs ├── LICENSE ├── tests │ ├── e2e_mcp.rs │ ├── integration_test.rs │ ├── e2e_set_permission_mode.rs │ ├── e2e_hooks.rs │ ├── e2e_set_model_and_end_input.rs │ ├── token_optimization_test.rs │ └── control_protocol_e2e.rs ├── Cargo.toml ├── src │ └── bin │ │ └── test_interactive.rs ├── test_new_features.sh └── docs │ └── HOOK_EVENT_NAMES.md ├── Cargo.toml ├── config ├── interactive.toml ├── fast.toml └── optimized.toml ├── .env.example ├── mcp_config.example.json ├── Dockerfile ├── .gitignore ├── LICENSE ├── docs ├── valid-models.md ├── url-preview-integration.md ├── url-preview-troubleshooting.md └── tool-calling.md ├── tests └── api_tests.rs ├── CONTROL_PROTOCOL_COMPATIBILITY.md ├── AGENTS.md └── doc └── INTERACTIVE_SESSION_GUIDE.md /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /claude-code-api/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error_handler; 2 | pub mod request_id; -------------------------------------------------------------------------------- /claude-code-api/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod openai; 2 | pub mod claude; 3 | pub mod error; 4 | 5 | #[cfg(test)] 6 | mod tests; -------------------------------------------------------------------------------- /claude-code-api/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod streaming; 2 | pub mod parser; 3 | pub mod function_calling; 4 | pub mod text_chunker; -------------------------------------------------------------------------------- /claude-code-api/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod models; 3 | pub mod projects; 4 | pub mod sessions; 5 | pub mod conversations; 6 | pub mod stats; 7 | pub mod streaming_handler; -------------------------------------------------------------------------------- /claude-code-api/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod claude_manager; 3 | pub mod session_manager; 4 | pub mod database; 5 | pub mod auth; 6 | pub mod process_pool; 7 | pub mod conversation; 8 | pub mod cache; 9 | pub mod retry; 10 | pub mod interactive_session; -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 编译并运行 Claude Code API 4 | 5 | echo "🔨 编译项目..." 6 | cargo build --release 7 | 8 | if [ $? -eq 0 ]; then 9 | echo "✅ 编译成功!" 10 | echo "🚀 启动服务器..." 11 | ./target/release/claude-code-api 12 | else 13 | echo "❌ 编译失败" 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /claude-code-sdk-rs/.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target/ 3 | Cargo.lock 4 | **/*.rs.bk 5 | 6 | # IDE 7 | .idea/ 8 | .vscode/ 9 | *.swp 10 | *.swo 11 | *~ 12 | 13 | # OS 14 | .DS_Store 15 | Thumbs.db 16 | 17 | # Python test files 18 | *.py 19 | __pycache__/ 20 | .pytest_cache/ 21 | 22 | # Test outputs 23 | test_output/ 24 | *.log 25 | 26 | # Documentation build 27 | doc/_build/ 28 | 29 | # Temporary files 30 | *.tmp 31 | *.temp 32 | .env -------------------------------------------------------------------------------- /script/start_with_permissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 启动带文件权限的 Claude Code API 4 | 5 | echo "Starting Claude Code API with file permissions..." 6 | 7 | # 方式1:使用环境变量跳过所有权限检查(不推荐在生产环境使用) 8 | # export CLAUDE_CODE__FILE_ACCESS__SKIP_PERMISSIONS=true 9 | 10 | # 方式2:添加特定目录的访问权限(推荐) 11 | export CLAUDE_CODE__FILE_ACCESS__ADDITIONAL_DIRS='["/Users/zhangalex/Work", "/Users/zhangalex/Documents", "/tmp"]' 12 | 13 | # 方式3:临时使用环境变量(最简单) 14 | export CLAUDE_SKIP_PERMISSIONS=true 15 | 16 | # 启动服务 17 | ./target/release/claude-code-api -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["claude-code-api", "claude-code-sdk-rs"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.2.0" 7 | edition = "2024" 8 | authors = ["blackanger"] 9 | license = "MIT" 10 | repository = "https://github.com/ZhangHanDong/claude-code-api-rs" 11 | 12 | [workspace.dependencies] 13 | tokio = { version = "1", features = ["full"] } 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = "1" 16 | anyhow = "1" 17 | thiserror = "1" 18 | tracing = "0.1" 19 | futures = "0.3" 20 | async-trait = "0.1" 21 | -------------------------------------------------------------------------------- /config/interactive.toml: -------------------------------------------------------------------------------- 1 | # 交互式会话测试配置 2 | [server] 3 | host = "0.0.0.0" 4 | port = 8080 5 | 6 | [claude] 7 | command = "claude" 8 | timeout_seconds = 300 9 | max_concurrent_sessions = 10 10 | use_interactive_sessions = true # 启用交互式会话 11 | 12 | [file_access] 13 | skip_permissions = true 14 | additional_dirs = [] 15 | 16 | [mcp] 17 | enabled = false 18 | 19 | [cache] 20 | enabled = true 21 | max_entries = 1000 22 | ttl_seconds = 3600 23 | 24 | [conversation] 25 | max_history_messages = 20 26 | session_timeout_minutes = 30 27 | 28 | [process_pool] 29 | size = 5 30 | min_idle = 2 31 | max_idle = 5 -------------------------------------------------------------------------------- /claude-code-api/src/core/database.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use sqlx::{sqlite::SqlitePool, Pool, Sqlite}; 4 | use anyhow::Result; 5 | 6 | pub struct Database { 7 | pool: Pool, 8 | } 9 | 10 | impl Database { 11 | pub async fn new(database_url: &str) -> Result { 12 | let pool = SqlitePool::connect(database_url).await?; 13 | Ok(Self { pool }) 14 | } 15 | 16 | pub async fn migrate(&self) -> Result<()> { 17 | // 暂时跳过迁移,可以后续添加实际的迁移文件 18 | Ok(()) 19 | } 20 | 21 | pub fn pool(&self) -> &Pool { 22 | &self.pool 23 | } 24 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | CLAUDE_CODE__SERVER__HOST=0.0.0.0 3 | CLAUDE_CODE__SERVER__PORT=8080 4 | 5 | # Claude Configuration 6 | CLAUDE_CODE__CLAUDE__COMMAND=claude 7 | CLAUDE_CODE__CLAUDE__TIMEOUT_SECONDS=300 8 | CLAUDE_CODE__CLAUDE__MAX_CONCURRENT_SESSIONS=10 9 | 10 | # Database Configuration 11 | CLAUDE_CODE__DATABASE__URL=sqlite://./claude_code.db 12 | CLAUDE_CODE__DATABASE__MAX_CONNECTIONS=5 13 | 14 | # Authentication Configuration 15 | CLAUDE_CODE__AUTH__ENABLED=false 16 | CLAUDE_CODE__AUTH__SECRET_KEY=change-me-in-production 17 | CLAUDE_CODE__AUTH__TOKEN_EXPIRY_HOURS=24 18 | 19 | # Logging 20 | RUST_LOG=claude_code_api=debug,tower_http=debug -------------------------------------------------------------------------------- /claude-code-api/src/api/sessions.rs: -------------------------------------------------------------------------------- 1 | use axum::{Json, response::IntoResponse}; 2 | use serde::{Deserialize, Serialize}; 3 | use crate::models::error::ApiResult; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct SessionInfo { 8 | pub id: String, 9 | pub project_path: Option, 10 | } 11 | 12 | #[allow(dead_code)] 13 | pub async fn list_sessions() -> ApiResult { 14 | let sessions: Vec = vec![]; 15 | Ok(Json(sessions)) 16 | } 17 | 18 | #[allow(dead_code)] 19 | pub async fn create_session() -> ApiResult { 20 | Ok(Json(serde_json::json!({ 21 | "message": "Not implemented" 22 | }))) 23 | } -------------------------------------------------------------------------------- /claude-code-api/src/api/projects.rs: -------------------------------------------------------------------------------- 1 | use axum::{Json, response::IntoResponse}; 2 | use serde::{Deserialize, Serialize}; 3 | use crate::models::error::ApiResult; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct Project { 8 | pub id: String, 9 | pub name: String, 10 | pub path: String, 11 | } 12 | 13 | #[allow(dead_code)] 14 | pub async fn list_projects() -> ApiResult { 15 | let projects: Vec = vec![]; 16 | Ok(Json(projects)) 17 | } 18 | 19 | #[allow(dead_code)] 20 | pub async fn create_project() -> ApiResult { 21 | Ok(Json(serde_json::json!({ 22 | "message": "Not implemented" 23 | }))) 24 | } -------------------------------------------------------------------------------- /script/start_with_mcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 启动带 MCP 支持的 Claude Code API 4 | 5 | echo "Starting Claude Code API with MCP support..." 6 | 7 | # 启用 MCP 8 | export CLAUDE_CODE__MCP__ENABLED=true 9 | 10 | # 使用配置文件(推荐) 11 | export CLAUDE_CODE__MCP__CONFIG_FILE="./mcp_config.json" 12 | 13 | # 或者使用 JSON 字符串 14 | # export CLAUDE_CODE__MCP__CONFIG_JSON='{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}}}' 15 | 16 | # 严格模式:只使用 MCP 配置中的服务器 17 | # export CLAUDE_CODE__MCP__STRICT=true 18 | 19 | # 调试模式:显示 MCP 服务器错误 20 | # export CLAUDE_CODE__MCP__DEBUG=true 21 | 22 | # 同时启用文件权限 23 | export CLAUDE_CODE__FILE_ACCESS__SKIP_PERMISSIONS=true 24 | 25 | # 启动服务 26 | ./target/release/claude-code-api -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/.env.example: -------------------------------------------------------------------------------- 1 | # Claude Code SDK Environment Variables 2 | # Copy this file to .env and update with your values 3 | 4 | # Account Information 5 | # Set this to reliably identify the account being used 6 | ANTHROPIC_USER_EMAIL=your-email@example.com 7 | 8 | # Optional: Override default Claude CLI path 9 | # CLAUDE_CODE_CLI_PATH=/usr/local/bin/claude 10 | 11 | # Optional: Set default model 12 | # CLAUDE_MODEL=claude-sonnet-4-5-20250929 13 | 14 | # Optional: Enable debug logging 15 | # RUST_LOG=debug 16 | # RUST_LOG=cc_sdk=debug 17 | 18 | # Optional: Set maximum output tokens (1-32000) 19 | # CLAUDE_CODE_MAX_OUTPUT_TOKENS=4000 20 | 21 | # Optional: Set API key (if using direct API mode) 22 | # ANTHROPIC_API_KEY=your-api-key-here 23 | -------------------------------------------------------------------------------- /claude-code-api/src/api/models.rs: -------------------------------------------------------------------------------- 1 | use axum::{Json, response::IntoResponse}; 2 | use chrono::Utc; 3 | use crate::models::{ 4 | openai::{Model, ModelList}, 5 | claude::ClaudeModel, 6 | error::ApiResult, 7 | }; 8 | 9 | pub async fn list_models() -> ApiResult { 10 | let claude_models = ClaudeModel::all(); 11 | 12 | let models: Vec = claude_models.into_iter() 13 | .map(|m| Model { 14 | id: m.id, 15 | object: "model".to_string(), 16 | created: Utc::now().timestamp(), 17 | owned_by: "anthropic".to_string(), 18 | }) 19 | .collect(); 20 | 21 | let response = ModelList { 22 | object: "list".to_string(), 23 | data: models, 24 | }; 25 | 26 | Ok(Json(response)) 27 | } -------------------------------------------------------------------------------- /claude-code-api/src/api/stats.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::State, 3 | response::IntoResponse, 4 | Json, 5 | }; 6 | use serde::Serialize; 7 | use std::sync::Arc; 8 | 9 | use crate::{ 10 | core::cache::ResponseCache, 11 | models::error::ApiResult, 12 | }; 13 | 14 | #[derive(Clone)] 15 | pub struct StatsState { 16 | pub cache: Arc, 17 | } 18 | 19 | #[derive(Debug, Serialize)] 20 | pub struct SystemStats { 21 | pub cache: crate::core::cache::CacheStats, 22 | pub version: &'static str, 23 | } 24 | 25 | pub async fn get_stats( 26 | State(state): State, 27 | ) -> ApiResult { 28 | let stats = SystemStats { 29 | cache: state.cache.stats(), 30 | version: env!("CARGO_PKG_VERSION"), 31 | }; 32 | 33 | Ok(Json(stats)) 34 | } -------------------------------------------------------------------------------- /config/fast.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | host = "0.0.0.0" 3 | port = 8000 4 | 5 | [claude] 6 | command = "claude-code" 7 | # 减少超时时间 8 | timeout_seconds = 120 9 | max_concurrent_sessions = 20 10 | # 使用交互式会话管理器以复用进程 11 | use_interactive_sessions = false # 使用进程池模式,每次请求创建新进程 12 | 13 | [process_pool] 14 | # 暂时禁用进程池预热 15 | size = 0 16 | min_idle = 0 17 | max_idle = 0 18 | 19 | [database] 20 | url = "sqlite://claude_code_api.db" 21 | max_connections = 25 22 | 23 | [auth] 24 | enabled = false 25 | secret_key = "your-secret-key" 26 | token_expiry_hours = 24 27 | 28 | [file_access] 29 | skip_permissions = true 30 | additional_dirs = [] 31 | 32 | [mcp] 33 | enabled = false 34 | 35 | [cache] 36 | enabled = true 37 | max_entries = 10000 38 | ttl_seconds = 14400 # 4小时 39 | 40 | [conversation] 41 | max_history_messages = 20 42 | session_timeout_minutes = 240 # 4小时 -------------------------------------------------------------------------------- /config/optimized.toml: -------------------------------------------------------------------------------- 1 | # 性能优化配置 2 | [server] 3 | host = "0.0.0.0" 4 | port = 8000 5 | 6 | [claude] 7 | command = "claude-code" 8 | timeout_seconds = 300 9 | max_concurrent_sessions = 20 10 | 11 | [process_pool] 12 | # 进程池大小 - 根据您的系统资源调整 13 | size = 10 14 | min_idle = 3 15 | max_idle = 8 16 | 17 | [database] 18 | url = "sqlite://claude_code_api.db" 19 | max_connections = 25 20 | 21 | [auth] 22 | enabled = false 23 | secret_key = "your-secret-key" 24 | token_expiry_hours = 24 25 | 26 | [file_access] 27 | # 跳过权限检查以提高性能(仅在信任的环境中使用) 28 | skip_permissions = true 29 | additional_dirs = [] 30 | 31 | [mcp] 32 | # 如果不需要 MCP,禁用它以提高性能 33 | enabled = false 34 | 35 | [cache] 36 | # 启用缓存以大幅提升重复查询的速度 37 | enabled = true 38 | max_entries = 5000 39 | ttl_seconds = 7200 # 2小时 40 | 41 | [conversation] 42 | max_history_messages = 50 43 | session_timeout_minutes = 120 -------------------------------------------------------------------------------- /mcp_config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "filesystem": { 4 | "command": "npx", 5 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp", "/Users/zhangalex/Work"], 6 | "env": {} 7 | }, 8 | "github": { 9 | "command": "npx", 10 | "args": ["-y", "@modelcontextprotocol/server-github"], 11 | "env": { 12 | "GITHUB_PERSONAL_ACCESS_TOKEN": "your-github-token-here" 13 | } 14 | }, 15 | "sqlite": { 16 | "command": "npx", 17 | "args": ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"], 18 | "env": {} 19 | }, 20 | "google-drive": { 21 | "command": "npx", 22 | "args": ["-y", "@modelcontextprotocol/server-gdrive"], 23 | "env": { 24 | "GOOGLE_CLIENT_ID": "your-client-id", 25 | "GOOGLE_CLIENT_SECRET": "your-client-secret" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /claude-code-api/src/bin/ccapi.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use std::env; 3 | use std::os::unix::process::CommandExt; 4 | 5 | fn main() { 6 | // Get the path to the claude-code-api binary 7 | let exe_path = env::current_exe() 8 | .expect("Failed to get current executable path"); 9 | 10 | let exe_dir = exe_path.parent() 11 | .expect("Failed to get executable directory"); 12 | 13 | let claude_code_api_path = exe_dir.join("claude-code-api"); 14 | 15 | // Forward all arguments to claude-code-api 16 | let args: Vec = env::args().skip(1).collect(); 17 | 18 | // Execute claude-code-api with the same arguments 19 | let _ = Command::new(claude_code_api_path) 20 | .args(args) 21 | .exec(); 22 | 23 | // If exec fails, print error 24 | eprintln!("Failed to execute claude-code-api"); 25 | std::process::exit(1); 26 | } -------------------------------------------------------------------------------- /claude-code-api/src/utils/streaming.rs: -------------------------------------------------------------------------------- 1 | use axum::response::sse::{Event, KeepAlive, Sse}; 2 | use futures::stream::{Stream, StreamExt}; 3 | use std::convert::Infallible; 4 | use std::time::Duration; 5 | use serde::Serialize; 6 | 7 | pub fn create_sse_stream(stream: S) -> Sse>> 8 | where 9 | S: Stream + Send + 'static, 10 | T: Serialize, 11 | { 12 | let event_stream = stream.map(|data| { 13 | Ok(Event::default() 14 | .data(serde_json::to_string(&data).unwrap_or_default())) 15 | }); 16 | 17 | Sse::new(event_stream) 18 | .keep_alive( 19 | KeepAlive::new() 20 | .interval(Duration::from_secs(30)) 21 | .text("keep-alive"), 22 | ) 23 | } 24 | 25 | #[allow(dead_code)] 26 | pub fn create_done_event() -> Event { 27 | Event::default().data("[DONE]") 28 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM rust:1.75-alpine AS builder 3 | 4 | RUN apk add --no-cache musl-dev 5 | 6 | WORKDIR /app 7 | 8 | # Copy manifests 9 | COPY Cargo.toml ./ 10 | 11 | # Build dependencies - this is the caching layer 12 | RUN mkdir src && \ 13 | echo "fn main() {}" > src/main.rs && \ 14 | cargo build --release && \ 15 | rm -rf src 16 | 17 | # Copy source code 18 | COPY src ./src 19 | 20 | # Build application 21 | RUN touch src/main.rs && \ 22 | cargo build --release 23 | 24 | # Runtime stage 25 | FROM alpine:latest 26 | 27 | RUN apk add --no-cache ca-certificates 28 | 29 | WORKDIR /app 30 | 31 | # Copy binary from builder 32 | COPY --from=builder /app/target/release/claude-code-api /usr/local/bin/claude-code-api 33 | 34 | # Create non-root user 35 | RUN addgroup -g 1000 claude && \ 36 | adduser -D -u 1000 -G claude claude 37 | 38 | USER claude 39 | 40 | EXPOSE 8080 41 | 42 | CMD ["claude-code-api"] -------------------------------------------------------------------------------- /claude-code-api/src/middleware/request_id.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::Request, 3 | http::HeaderName, 4 | middleware::Next, 5 | response::Response, 6 | }; 7 | use uuid::Uuid; 8 | 9 | pub static X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id"); 10 | 11 | pub async fn add_request_id( 12 | mut req: Request, 13 | next: Next, 14 | ) -> Response { 15 | let request_id = req 16 | .headers() 17 | .get(&X_REQUEST_ID) 18 | .and_then(|v| v.to_str().ok()) 19 | .map(String::from) 20 | .unwrap_or_else(|| Uuid::new_v4().to_string()); 21 | 22 | req.headers_mut().insert( 23 | X_REQUEST_ID.clone(), 24 | request_id.parse().unwrap(), 25 | ); 26 | 27 | let mut response = next.run(req).await; 28 | 29 | response.headers_mut().insert( 30 | X_REQUEST_ID.clone(), 31 | request_id.parse().unwrap(), 32 | ); 33 | 34 | response 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | # Generated by Cargo 3 | # will have compiled files and executables 4 | debug 5 | target 6 | 7 | # These are backup files generated by rustfmt 8 | **/*.rs.bk 9 | 10 | # MSVC Windows builds of rustc generate these, which store debugging information 11 | *.pdb 12 | 13 | # Generated by cargo mutants 14 | # Contains mutation testing data 15 | **/mutants.out*/ 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ 23 | ======= 24 | # Rust 25 | target/ 26 | Cargo.lock 27 | **/*.rs.bk 28 | 29 | # IDE 30 | .idea/ 31 | .vscode/ 32 | *.swp 33 | *.swo 34 | 35 | # Environment 36 | .env 37 | .env.local 38 | 39 | # Database 40 | *.db 41 | *.db-shm 42 | *.db-wal 43 | 44 | # Logs 45 | *.log 46 | 47 | # OS 48 | .DS_Store 49 | Thumbs.db 50 | 51 | IMPROVEMENTS.md 52 | >>>>>>> d8f74e1 (v 0.1.0) 53 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Claude Code SDK Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/claude-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "claude": { 3 | "model": "claude-3-opus-20240229", 4 | "maxTokens": 4096, 5 | "temperature": 0.7, 6 | "systemPrompt": "You are an expert software engineer with deep knowledge of Rust, Python, and modern software architecture.", 7 | "dangerouslySkipPermissions": false, 8 | "permissionMode": "acceptEdits", 9 | "maxThinkingTokens": 8000 10 | }, 11 | "tools": { 12 | "allowedTools": [ 13 | "Read", 14 | "Write", 15 | "Edit", 16 | "Grep", 17 | "Glob", 18 | "LS", 19 | "Bash" 20 | ], 21 | "disallowedTools": [] 22 | }, 23 | "mcp": { 24 | "servers": { 25 | "filesystem": { 26 | "type": "stdio", 27 | "command": "npx", 28 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] 29 | } 30 | } 31 | }, 32 | "directories": { 33 | "defaultWorkingDirectory": "/Users/zhangalex/Work/Projects", 34 | "additionalDirectories": [ 35 | "/Users/zhangalex/Documents", 36 | "/Users/zhangalex/Downloads" 37 | ] 38 | }, 39 | "logging": { 40 | "level": "info", 41 | "outputFormat": "stream-json" 42 | } 43 | } -------------------------------------------------------------------------------- /script/test_mcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 测试 MCP 功能的脚本 4 | 5 | echo "=== 测试 MCP 功能 ===" 6 | 7 | # 测试 1: 基本测试(不使用 MCP) 8 | echo -e "\n1. 基本测试(不使用 MCP):" 9 | curl -X POST http://localhost:8080/v1/chat/completions \ 10 | -H "Content-Type: application/json" \ 11 | -d '{ 12 | "model": "claude-opus-4-20250514", 13 | "messages": [{ 14 | "role": "user", 15 | "content": "列出可用的 MCP 服务器" 16 | }] 17 | }' | jq . 18 | 19 | # 测试 2: 使用 MCP 文件系统服务器 20 | echo -e "\n2. 测试 MCP 文件系统功能:" 21 | curl -X POST http://localhost:8080/v1/chat/completions \ 22 | -H "Content-Type: application/json" \ 23 | -d '{ 24 | "model": "claude-opus-4-20250514", 25 | "messages": [{ 26 | "role": "user", 27 | "content": "使用 MCP filesystem 服务器列出 /tmp 目录的内容" 28 | }] 29 | }' | jq . 30 | 31 | # 测试 3: 使用 MCP GitHub 服务器 32 | echo -e "\n3. 测试 MCP GitHub 功能:" 33 | curl -X POST http://localhost:8080/v1/chat/completions \ 34 | -H "Content-Type: application/json" \ 35 | -d '{ 36 | "model": "claude-opus-4-20250514", 37 | "messages": [{ 38 | "role": "user", 39 | "content": "使用 MCP github 服务器获取 anthropics/claude-code 仓库信息" 40 | }] 41 | }' | jq . 42 | 43 | echo -e "\n=== 测试完成 ===" -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/e2e_mcp.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{Query, Result, transport::mock::MockTransport}; 2 | use std::sync::Arc; 3 | use tokio::sync::Mutex; 4 | 5 | #[tokio::test] 6 | async fn e2e_mcp_server_not_found_sends_error() -> Result<()> { 7 | let (transport, mut handle) = MockTransport::pair(); 8 | let transport = Arc::new(Mutex::new(transport)); 9 | 10 | let mut q = Query::new(transport.clone(), false, None, None, std::collections::HashMap::new()); 11 | q.start().await?; 12 | 13 | // Send MCP message for a non-existent server 14 | let req = serde_json::json!({ 15 | "type": "control_request", 16 | "request_id": "req_mcp_1", 17 | "request": { 18 | "subtype": "mcp_message", 19 | "server_name": "no_such_server", 20 | "message": {"jsonrpc": "2.0", "id": 1, "method": "ping"} 21 | } 22 | }); 23 | handle.sdk_control_tx.send(req).await.unwrap(); 24 | 25 | let outer = handle.outbound_control_rx.recv().await.unwrap(); 26 | assert_eq!(outer["type"], "control_response"); 27 | let resp = &outer["response"]; 28 | assert_eq!(resp["subtype"], "error"); 29 | assert_eq!(resp["request_id"], "req_mcp_1"); 30 | assert!(resp["error"].as_str().unwrap_or("").contains("no_such_server")); 31 | 32 | Ok(()) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | <<<<<<< HEAD 4 | Copyright (c) 2025 Alex 5 | ======= 6 | Copyright (c) 2024 Claude Code API Contributors 7 | >>>>>>> d8f74e1 (v 0.1.0) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | <<<<<<< HEAD 26 | SOFTWARE. 27 | ======= 28 | SOFTWARE. 29 | >>>>>>> d8f74e1 (v 0.1.0) 30 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_new_options.rs: -------------------------------------------------------------------------------- 1 | //! Test example for new ClaudeCodeOptions fields 2 | 3 | use cc_sdk::{ClaudeCodeOptions, Result}; 4 | use std::path::PathBuf; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // Test building options with new fields 9 | let options = ClaudeCodeOptions::builder() 10 | .system_prompt("You are a helpful assistant") 11 | .model("claude-3-opus-20240229") 12 | .settings("/path/to/settings.json") 13 | .add_dir("/path/to/project1") 14 | .add_dir("/path/to/project2") 15 | .build(); 16 | 17 | println!("Options created successfully:"); 18 | println!(" Settings: {:?}", options.settings); 19 | println!(" Add dirs: {:?}", options.add_dirs); 20 | 21 | // Test with add_dirs method 22 | let dirs = vec![ 23 | PathBuf::from("/path/to/dir1"), 24 | PathBuf::from("/path/to/dir2"), 25 | PathBuf::from("/path/to/dir3"), 26 | ]; 27 | 28 | let options2 = ClaudeCodeOptions::builder() 29 | .add_dirs(dirs) 30 | .settings("global-settings.json") 31 | .build(); 32 | 33 | println!("\nOptions2 created successfully:"); 34 | println!(" Settings: {:?}", options2.settings); 35 | println!(" Add dirs: {:?}", options2.add_dirs); 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/custom-claude-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "claude": { 3 | "model": "claude-3-opus-20240229", 4 | "maxTokens": 8192, 5 | "temperature": 0.5, 6 | "systemPrompt": "You are a specialized AI assistant for multi-project code analysis and development.", 7 | "appendSystemPrompt": "Always provide concise, actionable insights when analyzing code across multiple projects.", 8 | "permissionMode": "acceptEdits", 9 | "maxThinkingTokens": 10000, 10 | "maxTurns": 20 11 | }, 12 | "tools": { 13 | "allowedTools": [ 14 | "Read", 15 | "Write", 16 | "Edit", 17 | "MultiEdit", 18 | "Grep", 19 | "Glob", 20 | "LS", 21 | "Bash", 22 | "WebSearch", 23 | "WebFetch" 24 | ], 25 | "disallowedTools": [], 26 | "permissionPromptToolName": "ApprovalRequired" 27 | }, 28 | "mcp": { 29 | "servers": { 30 | "git": { 31 | "type": "stdio", 32 | "command": "npx", 33 | "args": ["-y", "@modelcontextprotocol/server-git"], 34 | "env": { 35 | "GIT_SHOW_DIFF": "true" 36 | } 37 | }, 38 | "filesystem": { 39 | "type": "stdio", 40 | "command": "npx", 41 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/"] 42 | } 43 | } 44 | }, 45 | "session": { 46 | "continueConversation": false, 47 | "resumeId": null 48 | }, 49 | "performance": { 50 | "enableCaching": true, 51 | "connectionPoolSize": 5, 52 | "requestTimeout": 600 53 | } 54 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_paths.rs: -------------------------------------------------------------------------------- 1 | //! Test example to verify paths are correct 2 | 3 | use std::env; 4 | use std::path::Path; 5 | 6 | fn main() { 7 | println!("Path verification test\n"); 8 | println!("======================\n"); 9 | 10 | // Get current directory 11 | let current_dir = env::current_dir().expect("Failed to get current directory"); 12 | println!("Current directory: {}", current_dir.display()); 13 | 14 | // Check various paths 15 | let paths_to_check = vec![ 16 | "examples/claude-settings.json", 17 | "examples/custom-claude-settings.json", 18 | "claude-code-sdk-rs/examples/claude-settings.json", 19 | "claude-code-sdk-rs/examples/custom-claude-settings.json", 20 | ]; 21 | 22 | println!("\nChecking relative paths:"); 23 | for path_str in &paths_to_check { 24 | let path = Path::new(path_str); 25 | println!(" {} -> exists: {}", path_str, path.exists()); 26 | } 27 | 28 | println!("\nChecking absolute paths:"); 29 | for path_str in &paths_to_check { 30 | let abs_path = current_dir.join(path_str); 31 | println!(" {} -> exists: {}", abs_path.display(), abs_path.exists()); 32 | } 33 | 34 | // Show the correct absolute paths 35 | println!("\nCorrect absolute paths:"); 36 | let settings1 = current_dir.join("examples/claude-settings.json"); 37 | let settings2 = current_dir.join("examples/custom-claude-settings.json"); 38 | 39 | if settings1.exists() { 40 | println!("✓ {}", settings1.display()); 41 | } 42 | if settings2.exists() { 43 | println!("✓ {}", settings2.display()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cc-sdk" 3 | version = "0.4.0" 4 | edition = "2024" 5 | authors = ["ZhangHanDong"] 6 | license = "MIT" 7 | description = "Rust SDK for Claude Code CLI with full interactive capabilities" 8 | repository = "https://github.com/ZhangHanDong/claude-code-api-rs" 9 | documentation = "https://docs.rs/cc-sdk" 10 | readme = "README.md" 11 | keywords = ["claude", "claude-code", "sdk", "ai", "llm"] 12 | categories = ["api-bindings", "asynchronous", "development-tools"] 13 | exclude = ["examples/", "tests/", "doc/", "README_*.md", "*.sh", "*.py"] 14 | 15 | [dependencies] 16 | # Workspace dependencies 17 | tokio.workspace = true 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | thiserror.workspace = true 21 | tracing.workspace = true 22 | futures.workspace = true 23 | async-trait.workspace = true 24 | 25 | # Local dependencies 26 | tokio-stream = { version = "0.1", features = ["sync"] } 27 | bytes = "1" 28 | pin-project-lite = "0.2" 29 | which = "6" 30 | dirs = "5" 31 | uuid = { version = "1", features = ["v4", "serde"] } 32 | async-stream = "0.3" 33 | rand = "0.8" 34 | crossbeam-channel = "0.5" 35 | libc = "0.2" 36 | # For auto-downloading CLI 37 | reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false, optional = true } 38 | 39 | [features] 40 | default = ["auto-download"] 41 | # Enable automatic CLI download when not found 42 | auto-download = ["reqwest"] 43 | 44 | [dev-dependencies] 45 | tokio-test = "0.4" 46 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 47 | tempfile = "3" 48 | chrono = "0.4" 49 | regex = "1.10" 50 | axum = "0.6" 51 | tower-http = { version = "0.4", features = ["cors"] } 52 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/src/bin/test_interactive.rs: -------------------------------------------------------------------------------- 1 | //! Simple test for interactive client 2 | 3 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Result}; 4 | use futures::StreamExt; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // Set up simple println-based debugging 9 | unsafe { 10 | std::env::set_var("RUST_LOG", "cc_sdk=debug"); 11 | } 12 | 13 | println!("Creating client with default options..."); 14 | let options = ClaudeCodeOptions::default(); 15 | let mut client = ClaudeSDKClient::new(options); 16 | 17 | println!("Connecting..."); 18 | client.connect(None).await?; 19 | println!("Connected!"); 20 | 21 | println!("Sending message: What is 1+1?"); 22 | client 23 | .send_request("What is 1+1?".to_string(), None) 24 | .await?; 25 | println!("Message sent!"); 26 | 27 | println!("Receiving messages..."); 28 | let mut message_count = 0; 29 | let mut stream = client.receive_messages().await; 30 | 31 | while let Some(result) = stream.next().await { 32 | match result { 33 | Ok(message) => { 34 | message_count += 1; 35 | println!("Message {message_count}: {message:?}"); 36 | 37 | // Check if it's a result message 38 | if matches!(message, cc_sdk::Message::Result { .. }) { 39 | println!("Got result message, stopping..."); 40 | break; 41 | } 42 | } 43 | Err(e) => { 44 | eprintln!("Error receiving message: {e}"); 45 | break; 46 | } 47 | } 48 | } 49 | 50 | println!("Disconnecting..."); 51 | client.disconnect().await?; 52 | println!("Done!"); 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /script/check_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🔍 Checking Claude Code API configuration..." 4 | echo "" 5 | 6 | # 检查环境变量 7 | echo "📋 Environment variables:" 8 | echo "RUN_MODE=$RUN_MODE" 9 | echo "CLAUDE_CODE__CLAUDE__USE_INTERACTIVE_SESSIONS=$CLAUDE_CODE__CLAUDE__USE_INTERACTIVE_SESSIONS" 10 | echo "CLAUDE_CODE__PROCESS_POOL__SIZE=$CLAUDE_CODE__PROCESS_POOL__SIZE" 11 | echo "" 12 | 13 | # 检查配置文件 14 | echo "📄 Config files:" 15 | if [ -f "config/fast.toml" ]; then 16 | echo "config/fast.toml exists:" 17 | grep -E "use_interactive_sessions|size|min_idle" config/fast.toml 18 | else 19 | echo "config/fast.toml not found" 20 | fi 21 | echo "" 22 | 23 | if [ -f "config/optimized.toml" ]; then 24 | echo "config/optimized.toml exists:" 25 | grep -E "use_interactive_sessions|size|min_idle" config/optimized.toml 26 | else 27 | echo "config/optimized.toml not found" 28 | fi 29 | echo "" 30 | 31 | # 测试健康检查 32 | echo "🏥 Testing health endpoint:" 33 | curl -s http://localhost:8080/health || echo "Server not running" 34 | echo "" 35 | echo "" 36 | 37 | # 测试统计信息 38 | echo "📊 Stats endpoint:" 39 | curl -s http://localhost:8080/stats | jq . 2>/dev/null || echo "Failed to get stats" 40 | echo "" 41 | 42 | # 简单的测试请求 43 | echo "🧪 Test request with conversation_id:" 44 | RESPONSE=$(curl -s -X POST http://localhost:8080/v1/chat/completions \ 45 | -H "Content-Type: application/json" \ 46 | -d '{ 47 | "model": "claude-3-5-sonnet-20241022", 48 | "conversation_id": "debug-test-123", 49 | "messages": [{"role": "user", "content": "Say hello"}] 50 | }') 51 | 52 | if [ -n "$RESPONSE" ]; then 53 | echo "Response received:" 54 | echo "$RESPONSE" | jq '{conversation_id, model, usage}' 2>/dev/null || echo "$RESPONSE" 55 | else 56 | echo "No response received" 57 | fi -------------------------------------------------------------------------------- /claude-code-api/src/core/session_manager.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::collections::HashMap; 4 | use parking_lot::RwLock; 5 | use std::sync::Arc; 6 | use chrono::{DateTime, Utc}; 7 | use uuid::Uuid; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Session { 11 | pub id: String, 12 | pub project_path: Option, 13 | pub created_at: DateTime, 14 | pub updated_at: DateTime, 15 | } 16 | 17 | pub struct SessionManager { 18 | sessions: Arc>>, 19 | } 20 | 21 | impl SessionManager { 22 | pub fn new() -> Self { 23 | Self { 24 | sessions: Arc::new(RwLock::new(HashMap::new())), 25 | } 26 | } 27 | 28 | pub fn create_session(&self, project_path: Option) -> String { 29 | let session_id = Uuid::new_v4().to_string(); 30 | let now = Utc::now(); 31 | 32 | let session = Session { 33 | id: session_id.clone(), 34 | project_path, 35 | created_at: now, 36 | updated_at: now, 37 | }; 38 | 39 | self.sessions.write().insert(session_id.clone(), session); 40 | session_id 41 | } 42 | 43 | pub fn get_session(&self, session_id: &str) -> Option { 44 | self.sessions.read().get(session_id).cloned() 45 | } 46 | 47 | pub fn update_session(&self, session_id: &str) { 48 | if let Some(session) = self.sessions.write().get_mut(session_id) { 49 | session.updated_at = Utc::now(); 50 | } 51 | } 52 | 53 | pub fn remove_session(&self, session_id: &str) -> Option { 54 | self.sessions.write().remove(session_id) 55 | } 56 | 57 | pub fn list_sessions(&self) -> Vec { 58 | self.sessions.read().values().cloned().collect() 59 | } 60 | } -------------------------------------------------------------------------------- /claude-code-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "claude-code-api" 3 | version.workspace = true 4 | edition = "2024" 5 | authors.workspace = true 6 | license.workspace = true 7 | description = "OpenAI-compatible API gateway for Claude Code CLI" 8 | repository.workspace = true 9 | readme = "../README.md" 10 | keywords = ["claude-code", "api", "openai", "ai", "llm"] 11 | categories = [ 12 | "web-programming::http-server", 13 | "command-line-utilities", 14 | "development-tools", 15 | ] 16 | 17 | [[bin]] 18 | name = "claude-code-api" 19 | path = "src/main.rs" 20 | 21 | [[bin]] 22 | name = "ccapi" 23 | path = "src/bin/ccapi.rs" 24 | 25 | [dependencies] 26 | # Workspace dependencies 27 | tokio.workspace = true 28 | serde.workspace = true 29 | serde_json.workspace = true 30 | anyhow.workspace = true 31 | thiserror.workspace = true 32 | tracing.workspace = true 33 | futures.workspace = true 34 | 35 | # Local dependencies 36 | axum = { version = "0.7", features = ["http2", "json", "macros"] } 37 | tower = { version = "0.4", features = ["full"] } 38 | tower-http = { version = "0.5", features = ["cors", "trace"] } 39 | chrono = { version = "0.4", features = ["serde"] } 40 | uuid = { version = "1", features = ["v4", "serde"] } 41 | sqlx = { version = "0.7", features = [ 42 | "runtime-tokio-rustls", 43 | "sqlite", 44 | "chrono", 45 | "uuid", 46 | ] } 47 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 48 | async-stream = "0.3" 49 | bytes = "1" 50 | dotenv = "0.15" 51 | config = "0.14" 52 | argon2 = "0.5" 53 | jsonwebtoken = "9" 54 | base64 = "0.22" 55 | reqwest = { version = "0.12", features = ["json", "stream"] } 56 | tokio-stream = "0.1" 57 | dashmap = "6" 58 | parking_lot = "0.12" 59 | sha2 = "0.10" 60 | once_cell = "1" 61 | 62 | [dev-dependencies] 63 | axum-test = "15" 64 | mockall = "0.12" 65 | tempfile = "3" 66 | -------------------------------------------------------------------------------- /claude-code-api/src/middleware/error_handler.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::Request, 3 | http::StatusCode, 4 | middleware::Next, 5 | response::{IntoResponse, Response}, 6 | Json, 7 | }; 8 | use std::time::Instant; 9 | use tracing::{error, warn}; 10 | 11 | use crate::models::error::{ErrorResponse, ErrorDetail}; 12 | 13 | pub async fn handle_errors( 14 | req: Request, 15 | next: Next, 16 | ) -> Response { 17 | let start = Instant::now(); 18 | let path = req.uri().path().to_string(); 19 | let method = req.method().to_string(); 20 | 21 | let response = next.run(req).await; 22 | 23 | let elapsed = start.elapsed(); 24 | let status = response.status(); 25 | 26 | if status.is_server_error() { 27 | error!( 28 | "Server error: {} {} - Status: {} - Duration: {:?}", 29 | method, path, status, elapsed 30 | ); 31 | } else if status.is_client_error() && status != StatusCode::NOT_FOUND { 32 | warn!( 33 | "Client error: {} {} - Status: {} - Duration: {:?}", 34 | method, path, status, elapsed 35 | ); 36 | } 37 | 38 | response 39 | } 40 | 41 | #[allow(dead_code)] 42 | pub async fn handle_panic(err: Box) -> Response { 43 | let details = if let Some(s) = err.downcast_ref::() { 44 | s.clone() 45 | } else if let Some(s) = err.downcast_ref::<&str>() { 46 | s.to_string() 47 | } else { 48 | "Unknown panic".to_string() 49 | }; 50 | 51 | error!("Panic occurred: {}", details); 52 | 53 | let error_response = ErrorResponse { 54 | error: ErrorDetail { 55 | message: "Internal server error".to_string(), 56 | r#type: "internal_error".to_string(), 57 | param: None, 58 | code: Some("panic".to_string()), 59 | }, 60 | }; 61 | 62 | (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)).into_response() 63 | } -------------------------------------------------------------------------------- /claude-code-api/src/core/auth.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use axum::{ 4 | extract::Request, 5 | http::{header, StatusCode}, 6 | middleware::Next, 7 | response::Response, 8 | }; 9 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 10 | use serde::{Deserialize, Serialize}; 11 | use chrono::{Duration, Utc}; 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct Claims { 15 | pub sub: String, 16 | pub exp: i64, 17 | pub iat: i64, 18 | } 19 | 20 | pub struct AuthManager { 21 | secret: String, 22 | enabled: bool, 23 | } 24 | 25 | impl AuthManager { 26 | pub fn new(secret: String, enabled: bool) -> Self { 27 | Self { secret, enabled } 28 | } 29 | 30 | pub fn generate_token(&self, user_id: &str, expiry_hours: i64) -> Result { 31 | let now = Utc::now(); 32 | let exp = now + Duration::hours(expiry_hours); 33 | 34 | let claims = Claims { 35 | sub: user_id.to_string(), 36 | exp: exp.timestamp(), 37 | iat: now.timestamp(), 38 | }; 39 | 40 | encode(&Header::default(), &claims, &EncodingKey::from_secret(self.secret.as_bytes())) 41 | } 42 | 43 | pub fn verify_token(&self, token: &str) -> Result { 44 | decode::( 45 | token, 46 | &DecodingKey::from_secret(self.secret.as_bytes()), 47 | &Validation::default(), 48 | ).map(|data| data.claims) 49 | } 50 | } 51 | 52 | pub async fn auth_middleware( 53 | req: Request, 54 | next: Next, 55 | ) -> Result { 56 | let auth_header = req 57 | .headers() 58 | .get(header::AUTHORIZATION) 59 | .and_then(|h| h.to_str().ok()); 60 | 61 | if let Some(auth_header) = auth_header 62 | && auth_header.starts_with("Bearer ") { 63 | return Ok(next.run(req).await); 64 | } 65 | 66 | Err(StatusCode::UNAUTHORIZED) 67 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the Claude Code SDK 2 | 3 | use cc_sdk::{ClaudeCodeOptions, PermissionMode}; 4 | 5 | #[test] 6 | #[allow(deprecated)] 7 | fn test_options_builder() { 8 | let options = ClaudeCodeOptions::builder() 9 | .system_prompt("Test prompt") 10 | .model("claude-3-opus") 11 | .permission_mode(PermissionMode::AcceptEdits) 12 | .allow_tool("read") 13 | .allow_tool("write") 14 | .max_turns(10) 15 | .max_thinking_tokens(5000) 16 | .build(); 17 | 18 | assert_eq!(options.system_prompt, Some("Test prompt".to_string())); 19 | assert_eq!(options.model, Some("claude-3-opus".to_string())); 20 | assert_eq!(options.permission_mode, PermissionMode::AcceptEdits); 21 | assert_eq!(options.allowed_tools, vec!["read", "write"]); 22 | assert_eq!(options.max_turns, Some(10)); 23 | assert_eq!(options.max_thinking_tokens, 5000); 24 | } 25 | 26 | #[test] 27 | fn test_message_types() { 28 | use cc_sdk::{ContentBlock, Message, TextContent, UserMessage}; 29 | 30 | let user_msg = Message::User { 31 | message: UserMessage { 32 | content: "Hello".to_string(), 33 | }, 34 | }; 35 | 36 | match user_msg { 37 | Message::User { message } => { 38 | assert_eq!(message.content, "Hello"); 39 | } 40 | _ => panic!("Expected User message"), 41 | } 42 | 43 | let text_block = ContentBlock::Text(TextContent { 44 | text: "Response text".to_string(), 45 | }); 46 | 47 | match text_block { 48 | ContentBlock::Text(text) => { 49 | assert_eq!(text.text, "Response text"); 50 | } 51 | _ => panic!("Expected Text block"), 52 | } 53 | } 54 | 55 | #[test] 56 | fn test_error_types() { 57 | use cc_sdk::SdkError; 58 | 59 | let err = SdkError::timeout(30); 60 | assert!(err.is_recoverable()); 61 | assert!(!err.is_config_error()); 62 | 63 | let err = SdkError::ConfigError("Invalid config".into()); 64 | assert!(!err.is_recoverable()); 65 | assert!(err.is_config_error()); 66 | } 67 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/e2e_set_permission_mode.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{Query, transport::mock::MockTransport}; 2 | use std::sync::Arc; 3 | use tokio::sync::Mutex; 4 | 5 | #[tokio::test] 6 | async fn e2e_set_permission_mode_sends_control_request() { 7 | let (transport, mut handle) = MockTransport::pair(); 8 | let transport = Arc::new(Mutex::new(transport)); 9 | 10 | let mut q = Query::new(transport.clone(), true, None, None, std::collections::HashMap::new()); 11 | q.start().await.unwrap(); 12 | 13 | // Spawn a task to mock the CLI response 14 | let sdk_control_tx = handle.sdk_control_tx.clone(); 15 | let responder = tokio::spawn(async move { 16 | // Wait for the outbound control request 17 | let req = handle.outbound_control_request_rx.recv().await.unwrap(); 18 | 19 | // Extract request_id and send back a success response 20 | let request_id = req.get("request_id") 21 | .and_then(|v| v.as_str()) 22 | .unwrap_or("unknown") 23 | .to_string(); 24 | 25 | let response = serde_json::json!({ 26 | "type": "control_response", 27 | "response": { 28 | "request_id": request_id, 29 | "subtype": "success", 30 | "response": {} 31 | } 32 | }); 33 | 34 | sdk_control_tx.send(response).await.unwrap(); 35 | req 36 | }); 37 | 38 | // Change permission mode - it will now receive the mocked response 39 | let set_mode = q.set_permission_mode("acceptEdits"); 40 | 41 | // Wait for both the responder and set_permission_mode to complete 42 | let (req, result) = tokio::join!(responder, set_mode); 43 | let req = req.unwrap(); 44 | result.unwrap(); 45 | 46 | // Validate outbound control_request 47 | assert_eq!(req.get("type").and_then(|v| v.as_str()), Some("control_request")); 48 | let inner = req.get("request").cloned().unwrap_or(serde_json::json!({})); 49 | assert_eq!(inner.get("type").and_then(|v| v.as_str()), Some("set_permission_mode")); 50 | assert_eq!(inner.get("mode").and_then(|v| v.as_str()), Some("acceptEdits")); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/init_test.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Result}; 2 | use futures::StreamExt; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // env_logger::init(); // Skip if not available 7 | 8 | println!("=== Initialization Test ===\n"); 9 | 10 | let options = ClaudeCodeOptions::default(); 11 | let mut client = ClaudeSDKClient::new(options); 12 | 13 | println!("1. Connecting to Claude CLI..."); 14 | client.connect(Some("Test initialization".to_string())).await?; 15 | println!(" ✅ Connected successfully"); 16 | 17 | println!("\n2. Checking server info..."); 18 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 19 | 20 | if let Some(server_info) = client.get_server_info().await { 21 | println!(" ✅ Server info received:"); 22 | println!(" {}", serde_json::to_string_pretty(&server_info)?); 23 | } else { 24 | println!(" ⚠️ No server info available"); 25 | } 26 | 27 | println!("\n3. Sending test message..."); 28 | client.send_user_message("What is 2+2?".to_string()).await?; 29 | 30 | let mut messages = client.receive_messages().await; 31 | let mut msg_count = 0; 32 | 33 | while let Some(msg_result) = messages.next().await { 34 | msg_count += 1; 35 | match msg_result { 36 | Ok(msg) => { 37 | println!(" Message {msg_count}: {msg:?}"); 38 | if matches!(msg, cc_sdk::Message::Result { .. }) { 39 | println!(" ✅ Received result message"); 40 | break; 41 | } 42 | } 43 | Err(e) => { 44 | println!(" ❌ Error: {e}"); 45 | break; 46 | } 47 | } 48 | 49 | if msg_count > 10 { 50 | println!(" ⚠️ Stopping after 10 messages"); 51 | break; 52 | } 53 | } 54 | 55 | println!("\n4. Disconnecting..."); 56 | client.disconnect().await?; 57 | println!(" ✅ Disconnected successfully"); 58 | 59 | println!("\n=== Test Complete ==="); 60 | Ok(()) 61 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_client.rs: -------------------------------------------------------------------------------- 1 | //! Minimal test for ClaudeSDKClient 2 | //! 3 | //! This example tests basic client connectivity. 4 | 5 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Message, Result}; 6 | use futures::StreamExt; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // Initialize logging with debug level 11 | tracing_subscriber::fmt() 12 | .with_env_filter("cc_sdk=debug") 13 | .with_max_level(tracing::Level::DEBUG) 14 | .init(); 15 | 16 | println!("Testing ClaudeSDKClient...\n"); 17 | 18 | // Minimal options 19 | let options = ClaudeCodeOptions::default(); 20 | 21 | let mut client = ClaudeSDKClient::new(options); 22 | 23 | // Connect without initial prompt 24 | println!("Connecting (no initial prompt)..."); 25 | client.connect(None).await?; 26 | println!("Connected!\n"); 27 | 28 | // Send a simple message 29 | println!("Sending: What is 2 + 2?"); 30 | client 31 | .send_request("What is 2 + 2?".to_string(), None) 32 | .await?; 33 | 34 | // Receive response 35 | println!("Waiting for response..."); 36 | let mut messages = client.receive_messages().await; 37 | let mut message_count = 0; 38 | 39 | while let Some(msg) = messages.next().await { 40 | message_count += 1; 41 | 42 | match msg { 43 | Ok(ref message) => { 44 | println!("Received message #{message_count}: {message:?}"); 45 | 46 | // Break on result message 47 | if let Message::Result { .. } = message { 48 | break; 49 | } 50 | } 51 | Err(e) => { 52 | println!("Error receiving message: {e}"); 53 | break; 54 | } 55 | } 56 | 57 | // Safety: break after 10 messages to avoid infinite loop 58 | if message_count > 10 { 59 | println!("Breaking after 10 messages"); 60 | break; 61 | } 62 | } 63 | 64 | // Disconnect 65 | println!("\nDisconnecting..."); 66 | client.disconnect().await?; 67 | println!("Done!"); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/interactive_real_test.rs: -------------------------------------------------------------------------------- 1 | //! Interactive real API test 2 | 3 | use cc_sdk::{ClaudeCodeOptions, ContentBlock, InteractiveClient, Message, PermissionMode, Result}; 4 | use std::io::{self, Write}; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | println!("=== Interactive Real API Test ==="); 9 | println!("Type 'exit' to quit\n"); 10 | 11 | // Create client with options 12 | let options = ClaudeCodeOptions::builder() 13 | .permission_mode(PermissionMode::AcceptEdits) 14 | .model("claude-3.5-sonnet") 15 | .build(); 16 | 17 | let mut client = InteractiveClient::new(options)?; 18 | 19 | // Connect to Claude 20 | println!("Connecting to Claude..."); 21 | client.connect().await?; 22 | println!("Connected! You can start chatting.\n"); 23 | 24 | // Interactive loop 25 | loop { 26 | print!("You: "); 27 | io::stdout().flush().unwrap(); 28 | 29 | let mut input = String::new(); 30 | io::stdin().read_line(&mut input).unwrap(); 31 | let input = input.trim(); 32 | 33 | if input == "exit" { 34 | break; 35 | } 36 | 37 | if input.is_empty() { 38 | continue; 39 | } 40 | 41 | // Send message and get response 42 | match client.send_and_receive(input.to_string()).await { 43 | Ok(messages) => { 44 | print!("Claude: "); 45 | for msg in messages { 46 | if let Message::Assistant { message } = msg { 47 | for content in message.content { 48 | if let ContentBlock::Text(text) = content { 49 | println!("{}", text.text); 50 | } 51 | } 52 | } 53 | } 54 | println!(); 55 | } 56 | Err(e) => { 57 | println!("Error: {e}"); 58 | } 59 | } 60 | } 61 | 62 | // Disconnect 63 | println!("\nDisconnecting..."); 64 | client.disconnect().await?; 65 | println!("Goodbye!"); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/simple_real_test.rs: -------------------------------------------------------------------------------- 1 | //! Simple real API test - the easiest way to test 2 | 3 | use cc_sdk::{ClaudeCodeOptions, PermissionMode, Result, query}; 4 | use futures::StreamExt; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | println!("=== Simple Real API Test ===\n"); 9 | 10 | // Method 1: Using the simplest query function 11 | println!("Method 1: Simple query function"); 12 | println!("Query: What is 2 + 2?"); 13 | 14 | match query("What is 2 + 2?", None).await { 15 | Ok(mut stream) => { 16 | while let Some(result) = stream.next().await { 17 | match result { 18 | Ok(msg) => println!("Message: {msg:?}"), 19 | Err(e) => println!("Stream error: {e}"), 20 | } 21 | } 22 | } 23 | Err(e) => println!("Query error: {e}"), 24 | } 25 | 26 | println!("\n---\n"); 27 | 28 | // Method 2: Using query with options 29 | println!("Method 2: Query with custom options"); 30 | let options = ClaudeCodeOptions::builder() 31 | .permission_mode(PermissionMode::AcceptEdits) 32 | .model("claude-3.5-sonnet") 33 | .build(); 34 | 35 | println!("Query: Write a haiku about programming"); 36 | 37 | match query("Write a haiku about programming", Some(options)).await { 38 | Ok(mut stream) => { 39 | while let Some(result) = stream.next().await { 40 | match result { 41 | Ok(msg) => { 42 | // Extract text content from assistant messages 43 | if let cc_sdk::Message::Assistant { message } = msg { 44 | for content in message.content { 45 | if let cc_sdk::ContentBlock::Text(text) = content { 46 | println!("Response: {}", text.text); 47 | } 48 | } 49 | } 50 | } 51 | Err(e) => println!("Stream error: {e}"), 52 | } 53 | } 54 | } 55 | Err(e) => println!("Query error: {e}"), 56 | } 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /docs/valid-models.md: -------------------------------------------------------------------------------- 1 | # Valid Model Names for Claude Code API 2 | 3 | ## Overview 4 | 5 | Claude Code CLI uses specific model names that may differ from the standard Claude API model names. This document lists the valid model names that can be used with claude-code-api. 6 | 7 | ## Valid Model Names (2025) 8 | 9 | ### Recommended Aliases 10 | These aliases automatically use the latest version of each model family: 11 | - `opus` - Latest Opus model (currently 4.1) 12 | - `sonnet` - Latest Sonnet model (currently 4) 13 | 14 | ### Full Model Names 15 | - `claude-opus-4-1-20250805` - Claude Opus 4.1 (most capable) 16 | - `claude-sonnet-4-20250514` - Claude Sonnet 4 (balanced performance) 17 | - `claude-3-5-sonnet-20241022` - Claude 3.5 Sonnet (previous generation) 18 | - `claude-3-5-haiku-20241022` - Claude 3.5 Haiku (fastest) 19 | 20 | ## Invalid Model Names 21 | 22 | The following model names will result in 404 "not_found_error": 23 | - `opus-4.1` - Short alias not supported (use `opus` instead) 24 | - `opus-4` - Short alias not supported (use `opus` instead) 25 | - `sonnet-4` - Short alias not supported (use `sonnet` instead) 26 | - `claude-3-opus-20240229` - Outdated/invalid format 27 | 28 | ## Recommended Usage 29 | 30 | For best compatibility: 31 | 1. Use the full model names listed above 32 | 2. Use aliases `opus` or `sonnet` for convenience 33 | 3. Avoid using outdated model name formats 34 | 35 | ## Examples 36 | 37 | ### ✅ Valid Requests 38 | 39 | ```bash 40 | # Using recommended alias (BEST PRACTICE) 41 | curl -X POST http://localhost:8080/v1/chat/completions \ 42 | -H "Content-Type: application/json" \ 43 | -d '{ 44 | "model": "opus", 45 | "messages": [{"role": "user", "content": "Hello"}] 46 | }' 47 | 48 | # Using full model name 49 | curl -X POST http://localhost:8080/v1/chat/completions \ 50 | -H "Content-Type: application/json" \ 51 | -d '{ 52 | "model": "claude-opus-4-1-20250805", 53 | "messages": [{"role": "user", "content": "Hello"}] 54 | }' 55 | ``` 56 | 57 | ### ❌ Invalid Requests 58 | 59 | If you use an invalid model name like `opus-4.1` or `sonnet-4`, you'll get: 60 | ```json 61 | { 62 | "error": { 63 | "type": "invalid_request_error", 64 | "message": "system: Invalid model name" 65 | } 66 | } 67 | ``` -------------------------------------------------------------------------------- /tests/api_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use axum_test::TestServer; 4 | use serde_json::json; 5 | 6 | // 需要创建实际的应用实例来测试 7 | async fn create_test_app() -> axum::Router { 8 | // 这里应该返回你的实际应用路由 9 | // 暂时返回一个简单的路由 10 | use axum::{routing::get, Router}; 11 | 12 | Router::new() 13 | .route("/health", get(|| async { "OK" })) 14 | .route("/v1/models", get(|| async { 15 | axum::Json(json!({ 16 | "object": "list", 17 | "data": [] 18 | })) 19 | })) 20 | .route("/v1/chat/completions", axum::routing::post(|| async { 21 | axum::Json(json!({ 22 | "error": { 23 | "message": "Not implemented in test", 24 | "type": "test_error" 25 | } 26 | })) 27 | })) 28 | } 29 | 30 | #[tokio::test] 31 | async fn test_health_check() { 32 | let app = create_test_app().await; 33 | let server = TestServer::new(app).unwrap(); 34 | 35 | let response = server 36 | .get("/health") 37 | .await; 38 | 39 | response.assert_status_ok(); 40 | response.assert_text("OK"); 41 | } 42 | 43 | #[tokio::test] 44 | async fn test_list_models() { 45 | let app = create_test_app().await; 46 | let server = TestServer::new(app).unwrap(); 47 | 48 | let response = server 49 | .get("/v1/models") 50 | .await; 51 | 52 | response.assert_status_ok(); 53 | let json = response.json::(); 54 | assert_eq!(json["object"], "list"); 55 | assert!(json["data"].is_array()); 56 | } 57 | 58 | #[tokio::test] 59 | async fn test_chat_completion_missing_messages() { 60 | let app = create_test_app().await; 61 | let server = TestServer::new(app).unwrap(); 62 | 63 | let response = server 64 | .post("/v1/chat/completions") 65 | .json(&json!({ 66 | "model": "claude-3-opus-20240229", 67 | "messages": [] 68 | })) 69 | .await; 70 | 71 | // 在测试中我们返回的是200,实际应用中应该是400 72 | response.assert_status_ok(); 73 | } 74 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_settings.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating the use of the settings parameter 2 | //! 3 | //! This example shows how to use a custom settings file with Claude Code 4 | 5 | use cc_sdk::{ClaudeCodeOptions, Result, query}; 6 | use futures::StreamExt; 7 | use std::env; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=debug") 14 | .init(); 15 | 16 | println!("Testing settings parameter...\n"); 17 | 18 | // Get absolute path for settings file 19 | let current_dir = env::current_dir().expect("Failed to get current directory"); 20 | let settings_path = current_dir.join("examples/claude-settings.json"); 21 | let settings_str = settings_path.to_str().expect("Invalid path"); 22 | 23 | println!("Using settings file: {settings_str}"); 24 | 25 | // Create options with a custom settings file 26 | let options = ClaudeCodeOptions::builder() 27 | .settings(settings_str) // Use absolute path 28 | .system_prompt("You are a helpful assistant") 29 | .model("claude-3-opus-20240229") 30 | .permission_mode(cc_sdk::PermissionMode::AcceptEdits) 31 | .build(); 32 | println!("Querying Claude Code with custom settings...\n"); 33 | 34 | // Make a simple query 35 | let mut messages = query( 36 | "What are the benefits of using a settings file in Claude Code?", 37 | Some(options), 38 | ) 39 | .await?; 40 | 41 | // Process the response 42 | while let Some(msg) = messages.next().await { 43 | match msg? { 44 | cc_sdk::Message::Assistant { message } => { 45 | for block in message.content { 46 | if let cc_sdk::ContentBlock::Text(text) = block { 47 | println!("Claude: {}", text.text); 48 | } 49 | } 50 | } 51 | cc_sdk::Message::Result { 52 | duration_ms, 53 | total_cost_usd, 54 | .. 55 | } => { 56 | println!("\n---"); 57 | println!("Completed in {duration_ms}ms"); 58 | if let Some(cost) = total_cost_usd { 59 | println!("Cost: ${cost:.6}"); 60 | } 61 | } 62 | _ => {} 63 | } 64 | } 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /script/test_multimodal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 测试多模态功能的脚本 4 | 5 | echo "=== 测试多模态 API ===" 6 | 7 | # 测试1: 使用本地文件路径 8 | echo -e "\n1. 测试本地文件路径:" 9 | curl -X POST http://localhost:8080/v1/chat/completions \ 10 | -H "Content-Type: application/json" \ 11 | -d '{ 12 | "model": "claude-opus-4-20250514", 13 | "messages": [ 14 | { 15 | "role": "user", 16 | "content": [ 17 | {"type": "text", "text": "描述这张图片"}, 18 | {"type": "image_url", "image_url": {"url": "/tmp/test_image.png"}} 19 | ] 20 | } 21 | ] 22 | }' | jq . 23 | 24 | # 测试2: 使用远程图片 URL 25 | echo -e "\n2. 测试远程图片 URL:" 26 | curl -X POST http://localhost:8080/v1/chat/completions \ 27 | -H "Content-Type: application/json" \ 28 | -d '{ 29 | "model": "claude-opus-4-20250514", 30 | "messages": [ 31 | { 32 | "role": "user", 33 | "content": [ 34 | {"type": "text", "text": "这是什么标志?"}, 35 | {"type": "image_url", "image_url": {"url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png"}} 36 | ] 37 | } 38 | ] 39 | }' | jq . 40 | 41 | # 测试3: 使用 base64 编码的图片 (创建一个简单的1x1像素红色图片) 42 | echo -e "\n3. 测试 base64 图片:" 43 | BASE64_IMAGE="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" 44 | 45 | curl -X POST http://localhost:8080/v1/chat/completions \ 46 | -H "Content-Type: application/json" \ 47 | -d "{ 48 | \"model\": \"claude-opus-4-20250514\", 49 | \"messages\": [ 50 | { 51 | \"role\": \"user\", 52 | \"content\": [ 53 | {\"type\": \"text\", \"text\": \"这是什么颜色的像素?\"}, 54 | {\"type\": \"image_url\", \"image_url\": {\"url\": \"$BASE64_IMAGE\"}} 55 | ] 56 | } 57 | ] 58 | }" | jq . 59 | 60 | # 测试4: 多图片测试 61 | echo -e "\n4. 测试多张图片:" 62 | curl -X POST http://localhost:8080/v1/chat/completions \ 63 | -H "Content-Type: application/json" \ 64 | -d '{ 65 | "model": "claude-opus-4-20250514", 66 | "messages": [ 67 | { 68 | "role": "user", 69 | "content": [ 70 | {"type": "text", "text": "比较这两张图片"}, 71 | {"type": "image_url", "image_url": {"url": "/tmp/test1.png"}}, 72 | {"type": "image_url", "image_url": {"url": "/tmp/test2.png"}} 73 | ] 74 | } 75 | ] 76 | }' | jq . 77 | 78 | echo -e "\n=== 测试完成 ===" -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_process_leak.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{query, Result}; 2 | use futures::StreamExt; 3 | use std::process::Command; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | println!("Testing process leak fix...\n"); 8 | 9 | // Check initial Claude processes 10 | let initial_count = count_claude_processes(); 11 | println!("Initial Claude processes: {initial_count}"); 12 | 13 | // Run multiple queries 14 | for i in 1..=5 { 15 | println!("\n--- Query {i} ---"); 16 | 17 | // Create a query and consume only the first message 18 | let mut messages = query(format!("Say 'Test {i}'"), None).await?; 19 | 20 | // Only take the first message then drop the stream 21 | if let Some(msg) = messages.next().await { 22 | match msg { 23 | Ok(m) => println!("Got message: {:?}", std::mem::discriminant(&m)), 24 | Err(e) => println!("Error: {e}"), 25 | } 26 | } 27 | 28 | // Stream is dropped here, should trigger cleanup 29 | drop(messages); 30 | 31 | // Give some time for cleanup 32 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 33 | 34 | let current_count = count_claude_processes(); 35 | println!("Claude processes after query {i}: {current_count}"); 36 | 37 | if current_count > initial_count + 1 { 38 | println!("⚠️ WARNING: Process leak detected! Expected at most {} processes, found {}", 39 | initial_count + 1, current_count); 40 | } 41 | } 42 | 43 | // Final check after a delay 44 | println!("\n--- Final check after 2 seconds ---"); 45 | tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 46 | 47 | let final_count = count_claude_processes(); 48 | println!("Final Claude processes: {final_count}"); 49 | 50 | if final_count > initial_count { 51 | println!("❌ FAILED: Process leak detected! {} zombie processes remain", 52 | final_count - initial_count); 53 | } else { 54 | println!("✅ SUCCESS: No process leak detected!"); 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn count_claude_processes() -> usize { 61 | let output = Command::new("sh") 62 | .arg("-c") 63 | .arg("ps aux | grep -v grep | grep claude | wc -l") 64 | .output() 65 | .expect("Failed to execute ps command"); 66 | 67 | let count_str = String::from_utf8_lossy(&output.stdout); 68 | count_str.trim().parse().unwrap_or(0) 69 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/README_MODELS.md: -------------------------------------------------------------------------------- 1 | # Model Usage Guide for Examples 2 | 3 | ## Important Update (2025) 4 | 5 | All examples have been updated to use the correct model names based on testing results. 6 | 7 | ## ✅ Use These Model Names 8 | 9 | ```rust 10 | // RECOMMENDED - Use aliases for automatic latest version 11 | .model("sonnet") // Latest Sonnet (currently Sonnet 4) 12 | .model("opus") // Latest Opus (currently Opus 4.1) 13 | 14 | // Or use full names for specific versions 15 | .model("claude-opus-4-1-20250805") // Opus 4.1 16 | .model("claude-sonnet-4-20250514") // Sonnet 4 17 | ``` 18 | 19 | ## ❌ DO NOT Use These (They Return 404 Errors) 20 | 21 | ```rust 22 | .model("opus-4.1") // ❌ NOT SUPPORTED 23 | .model("sonnet-4") // ❌ NOT SUPPORTED 24 | .model("opus-4") // ❌ NOT SUPPORTED 25 | ``` 26 | 27 | ## Default Model in Examples 28 | 29 | All examples now use `"sonnet"` as the default model for optimal performance. 30 | 31 | ## Quick Test 32 | 33 | Run this to verify which models work on your system: 34 | ```bash 35 | cargo run --example simple_model_test 36 | ``` 37 | 38 | ## Example Usage 39 | 40 | ```rust 41 | use cc_sdk::{query, ClaudeCodeOptions, PermissionMode, Result}; 42 | 43 | #[tokio::main] 44 | async fn main() -> Result<()> { 45 | // Best practice: Use the alias 46 | let options = ClaudeCodeOptions::builder() 47 | .model("sonnet") // Recommended 48 | .permission_mode(PermissionMode::Plan) // v0.1.7 feature 49 | .build(); 50 | 51 | let mut messages = query("Hello, Claude!", Some(options)).await?; 52 | // ... process messages 53 | 54 | Ok(()) 55 | } 56 | ``` 57 | 58 | ## Model Selection Strategy 59 | 60 | 1. **For most tasks**: Use `"sonnet"` (balanced performance) 61 | 2. **For complex reasoning**: Use `"opus"` (most capable) 62 | 3. **For fastest response**: Use `"sonnet"` or older models 63 | 4. **For production**: Always implement fallback logic 64 | 65 | ## Fallback Pattern 66 | 67 | ```rust 68 | async fn query_with_fallback(prompt: &str) -> Result<()> { 69 | let models = vec!["opus", "sonnet"]; // Try opus first, fallback to sonnet 70 | 71 | for model in models { 72 | let options = ClaudeCodeOptions::builder() 73 | .model(model) 74 | .build(); 75 | 76 | match query(prompt, Some(options)).await { 77 | Ok(messages) => { 78 | // Process messages 79 | return Ok(()); 80 | } 81 | Err(_) => continue, // Try next model 82 | } 83 | } 84 | 85 | Err("All models failed".into()) 86 | } 87 | ``` -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/interactive_demo.rs: -------------------------------------------------------------------------------- 1 | //! Interactive demo showing stateful conversation 2 | 3 | use cc_sdk::{ClaudeCodeOptions, ContentBlock, Result, SimpleInteractiveClient}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | println!("=== Claude Interactive Demo ==="); 8 | println!("This demonstrates stateful conversations where Claude remembers context\n"); 9 | 10 | // Create and connect client 11 | let mut client = SimpleInteractiveClient::new(ClaudeCodeOptions::default())?; 12 | client.connect().await?; 13 | 14 | // Conversation 1: Establish context 15 | println!("User: My name is Alice and I like programming in Rust."); 16 | let messages = client 17 | .send_and_receive("My name is Alice and I like programming in Rust.".to_string()) 18 | .await?; 19 | 20 | print!("Claude: "); 21 | for msg in &messages { 22 | if let cc_sdk::Message::Assistant { message } = msg { 23 | for content in &message.content { 24 | if let ContentBlock::Text(text) = content { 25 | print!("{}", text.text); 26 | } 27 | } 28 | } 29 | } 30 | println!("\n"); 31 | 32 | // Conversation 2: Test memory 33 | println!("User: What's my name and what language do I like?"); 34 | let messages = client 35 | .send_and_receive("What's my name and what language do I like?".to_string()) 36 | .await?; 37 | 38 | print!("Claude: "); 39 | for msg in &messages { 40 | if let cc_sdk::Message::Assistant { message } = msg { 41 | for content in &message.content { 42 | if let ContentBlock::Text(text) = content { 43 | print!("{}", text.text); 44 | } 45 | } 46 | } 47 | } 48 | println!("\n"); 49 | 50 | // Conversation 3: Continue context 51 | println!("User: Can you give me a simple Rust tip?"); 52 | let messages = client 53 | .send_and_receive("Can you give me a simple Rust tip?".to_string()) 54 | .await?; 55 | 56 | print!("Claude: "); 57 | for msg in &messages { 58 | if let cc_sdk::Message::Assistant { message } = msg { 59 | for content in &message.content { 60 | if let ContentBlock::Text(text) = content { 61 | print!("{}", text.text); 62 | } 63 | } 64 | } 65 | } 66 | println!("\n"); 67 | 68 | // Disconnect 69 | client.disconnect().await?; 70 | println!("=== Demo Complete - Claude remembered the conversation context! ==="); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /claude-code-api/src/models/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use super::super::openai::*; 4 | use super::super::claude::*; 5 | 6 | #[test] 7 | fn test_chat_message_serialization() { 8 | let message = ChatMessage { 9 | role: "user".to_string(), 10 | content: Some(MessageContent::Text("Hello".to_string())), 11 | name: None, 12 | tool_calls: None, 13 | }; 14 | 15 | let json = serde_json::to_string(&message).unwrap(); 16 | assert!(json.contains("\"role\":\"user\"")); 17 | assert!(json.contains("\"content\":\"Hello\"")); 18 | } 19 | 20 | #[test] 21 | fn test_claude_model_list() { 22 | let models = ClaudeModel::all(); 23 | assert_eq!(models.len(), 8); // 3 Claude 4 + 2 Claude 3.7 + 2 Claude 3.5 + 1 Claude 3 24 | 25 | let model_ids: Vec = models.iter().map(|m| m.id.clone()).collect(); 26 | // Check Claude 4 models 27 | assert!(model_ids.contains(&"claude-opus-4-1-20250805".to_string())); 28 | assert!(model_ids.contains(&"claude-opus-4-20250514".to_string())); 29 | assert!(model_ids.contains(&"claude-sonnet-4-20250514".to_string())); 30 | // Check Claude 3.7 models 31 | assert!(model_ids.contains(&"claude-3-7-sonnet-20250219".to_string())); 32 | assert!(model_ids.contains(&"claude-3-7-sonnet-latest".to_string())); 33 | // Check Claude 3.5 models 34 | assert!(model_ids.contains(&"claude-3-5-haiku-20241022".to_string())); 35 | assert!(model_ids.contains(&"claude-3-5-haiku-latest".to_string())); 36 | // Check Claude 3 models 37 | assert!(model_ids.contains(&"claude-3-haiku-20240307".to_string())); 38 | } 39 | 40 | #[test] 41 | fn test_message_content_variants() { 42 | let text_content = MessageContent::Text("Hello".to_string()); 43 | let array_content = MessageContent::Array(vec![ 44 | ContentPart::Text { text: "Hello".to_string() }, 45 | ContentPart::ImageUrl { 46 | image_url: ImageUrl { 47 | url: "https://example.com/image.png".to_string(), 48 | detail: Some("high".to_string()), 49 | } 50 | }, 51 | ]); 52 | 53 | let text_json = serde_json::to_string(&text_content).unwrap(); 54 | assert_eq!(text_json, "\"Hello\""); 55 | 56 | let array_json = serde_json::to_string(&array_content).unwrap(); 57 | assert!(array_json.contains("\"type\":\"text\"")); 58 | assert!(array_json.contains("\"type\":\"image_url\"")); 59 | } 60 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/simple_query.rs: -------------------------------------------------------------------------------- 1 | //! Simple query example 2 | //! 3 | //! This example demonstrates how to use the simple `query` function 4 | //! for one-shot interactions with Claude. 5 | 6 | use cc_sdk::{ClaudeCodeOptions, Message, Result, query}; 7 | use futures::StreamExt; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=debug,simple_query=info") 14 | .init(); 15 | 16 | println!("Claude Code SDK - Simple Query Example\n"); 17 | 18 | // Example 1: Basic query 19 | println!("Example 1: Basic query"); 20 | println!("----------------------"); 21 | 22 | let mut messages = query("What is 2 + 2?", None).await?; 23 | 24 | while let Some(msg) = messages.next().await { 25 | match msg? { 26 | Message::Assistant { message } => { 27 | for block in &message.content { 28 | println!("Assistant: {block:?}"); 29 | } 30 | } 31 | Message::Result { duration_ms, .. } => { 32 | println!("Query completed in {duration_ms}ms"); 33 | break; 34 | } 35 | _ => {} 36 | } 37 | } 38 | 39 | println!("\n"); 40 | 41 | // Example 2: Query with options 42 | println!("Example 2: Query with custom options"); 43 | println!("------------------------------------"); 44 | 45 | let options = ClaudeCodeOptions::builder() 46 | .system_prompt("You are a helpful coding assistant. Keep responses concise.") 47 | .model("sonnet") 48 | .max_thinking_tokens(1000) 49 | .build(); 50 | 51 | let mut messages = query("Show me a hello world program in Rust", Some(options)).await?; 52 | 53 | while let Some(msg) = messages.next().await { 54 | match msg? { 55 | Message::Assistant { message } => { 56 | for block in &message.content { 57 | println!("Assistant: {block:?}"); 58 | } 59 | } 60 | Message::System { subtype, .. } => { 61 | println!("System: {subtype}"); 62 | } 63 | Message::Result { 64 | duration_ms, 65 | total_cost_usd, 66 | .. 67 | } => { 68 | println!("\nQuery completed in {duration_ms}ms"); 69 | if let Some(cost) = total_cost_usd { 70 | println!("Cost: ${cost:.4}"); 71 | } 72 | break; 73 | } 74 | _ => {} 75 | } 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/basic_client.rs: -------------------------------------------------------------------------------- 1 | //! Most basic client test 2 | //! 3 | //! Tests the absolute minimum functionality. 4 | 5 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Message, Result}; 6 | use futures::StreamExt; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | println!("Basic Client Test\n"); 11 | 12 | // Minimal setup 13 | let options = ClaudeCodeOptions::default(); 14 | let mut client = ClaudeSDKClient::new(options); 15 | 16 | // Connect 17 | println!("Connecting..."); 18 | client.connect(None).await?; 19 | println!("Connected!"); 20 | 21 | // Send message 22 | println!("\nSending: What is 1+1?"); 23 | client 24 | .send_request("What is 1+1?".to_string(), None) 25 | .await?; 26 | 27 | // Receive with timeout 28 | println!("Waiting for response (max 15 seconds)..."); 29 | 30 | let result = tokio::time::timeout(std::time::Duration::from_secs(15), async { 31 | let mut messages = client.receive_messages().await; 32 | let mut count = 0; 33 | 34 | while let Some(msg) = messages.next().await { 35 | count += 1; 36 | println!("\nMessage {count}: "); 37 | 38 | match msg { 39 | Ok(Message::Assistant { message }) => { 40 | for block in &message.content { 41 | if let cc_sdk::ContentBlock::Text(text) = block { 42 | println!("Assistant: {}", text.text); 43 | } 44 | } 45 | } 46 | Ok(Message::System { subtype, .. }) => { 47 | println!("System: {subtype}"); 48 | } 49 | Ok(Message::Result { duration_ms, .. }) => { 50 | println!("Completed in {duration_ms}ms"); 51 | return Ok(()); 52 | } 53 | Ok(Message::User { .. }) => { 54 | println!("User message (unexpected)"); 55 | } 56 | Err(e) => { 57 | println!("Error: {e}"); 58 | return Err(e); 59 | } 60 | } 61 | } 62 | 63 | println!("Stream ended without result message"); 64 | Ok(()) 65 | }) 66 | .await; 67 | 68 | match result { 69 | Ok(Ok(())) => println!("\nSuccess!"), 70 | Ok(Err(e)) => println!("\nError: {e}"), 71 | Err(_) => println!("\nTimeout - no response within 15 seconds"), 72 | } 73 | 74 | // Disconnect 75 | println!("\nDisconnecting..."); 76 | client.disconnect().await?; 77 | println!("Done!"); 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/test_new_features.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test script for new features in v0.1.6 4 | 5 | echo "==================================" 6 | echo "Testing Claude Code SDK v0.1.6" 7 | echo "==================================" 8 | echo "" 9 | 10 | # Colors for output 11 | GREEN='\033[0;32m' 12 | RED='\033[0;31m' 13 | NC='\033[0m' # No Color 14 | 15 | # Test 1: Basic compilation (only check new examples) 16 | echo "Test 1: Checking new examples compilation..." 17 | all_compile=true 18 | for example in test_new_options test_settings test_add_dirs test_combined_features; do 19 | if cargo check --example $example 2>&1 | grep -q "error"; then 20 | echo -e "${RED}✗ $example compilation failed${NC}" 21 | all_compile=false 22 | else 23 | echo -e "${GREEN}✓ $example compiles successfully${NC}" 24 | fi 25 | done 26 | echo "" 27 | 28 | # Test 2: Test new options example 29 | echo "Test 2: Running test_new_options example..." 30 | if cargo run --example test_new_options > /dev/null 2>&1; then 31 | echo -e "${GREEN}✓ test_new_options runs successfully${NC}" 32 | else 33 | echo -e "${RED}✗ test_new_options failed${NC}" 34 | fi 35 | echo "" 36 | 37 | # Test 3: Build documentation 38 | echo "Test 3: Building documentation..." 39 | if cargo doc --no-deps 2>&1 | grep -q "error"; then 40 | echo -e "${RED}✗ Documentation build failed${NC}" 41 | else 42 | echo -e "${GREEN}✓ Documentation builds successfully${NC}" 43 | fi 44 | echo "" 45 | 46 | # Test 4: Check for required files 47 | echo "Test 4: Checking example files..." 48 | files_to_check=( 49 | "examples/test_settings.rs" 50 | "examples/test_add_dirs.rs" 51 | "examples/test_combined_features.rs" 52 | "examples/test_new_options.rs" 53 | "examples/claude-settings.json" 54 | "examples/custom-claude-settings.json" 55 | ) 56 | 57 | all_files_exist=true 58 | for file in "${files_to_check[@]}"; do 59 | if [ -f "$file" ]; then 60 | echo -e "${GREEN}✓ $file exists${NC}" 61 | else 62 | echo -e "${RED}✗ $file not found${NC}" 63 | all_files_exist=false 64 | fi 65 | done 66 | echo "" 67 | 68 | # Summary 69 | echo "==================================" 70 | echo "Test Summary" 71 | echo "==================================" 72 | if [ "$all_files_exist" = true ]; then 73 | echo -e "${GREEN}All tests passed! The SDK is ready for testing.${NC}" 74 | echo "" 75 | echo "You can now test the new features with:" 76 | echo " cargo run --example test_settings" 77 | echo " cargo run --example test_add_dirs" 78 | echo " cargo run --example test_combined_features" 79 | else 80 | echo -e "${RED}Some tests failed. Please check the errors above.${NC}" 81 | fi -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/account_info.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating how to retrieve account information 2 | //! 3 | //! This example shows how to use the `get_account_info()` method to retrieve 4 | //! the current Claude account information including email, subscription type, etc. 5 | //! 6 | //! # Usage 7 | //! 8 | //! Set the ANTHROPIC_USER_EMAIL environment variable before running: 9 | //! 10 | //! ```bash 11 | //! export ANTHROPIC_USER_EMAIL="your-email@example.com" 12 | //! cargo run --example account_info 13 | //! ``` 14 | //! 15 | //! Or run directly with the environment variable: 16 | //! 17 | //! ```bash 18 | //! ANTHROPIC_USER_EMAIL="your-email@example.com" cargo run --example account_info 19 | //! ``` 20 | 21 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Result}; 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<()> { 25 | println!("╔═══════════════════════════════════════════╗"); 26 | println!("║ Claude Code Account Information ║"); 27 | println!("╚═══════════════════════════════════════════╝\n"); 28 | 29 | // Check if environment variable is set 30 | if let Ok(email) = std::env::var("ANTHROPIC_USER_EMAIL") { 31 | println!("ℹ️ Using ANTHROPIC_USER_EMAIL: {}\n", email); 32 | } else { 33 | println!("⚠️ ANTHROPIC_USER_EMAIL not set"); 34 | println!(" Run with: ANTHROPIC_USER_EMAIL=\"your@email.com\" cargo run --example account_info\n"); 35 | } 36 | 37 | // Create client with default options 38 | let options = ClaudeCodeOptions::builder() 39 | .max_turns(1) // Limit to 1 turn since we only need account info 40 | .build(); 41 | 42 | let mut client = ClaudeSDKClient::new(options); 43 | 44 | println!("1. Connecting to Claude CLI..."); 45 | client.connect(None).await?; 46 | println!(" ✅ Connected\n"); 47 | 48 | println!("2. Retrieving account information..."); 49 | match client.get_account_info().await { 50 | Ok(account_info) => { 51 | println!(" ✅ Account information retrieved:\n"); 52 | println!("╔═══════════════════════════════════════════╗"); 53 | println!("║ Account Details ║"); 54 | println!("╠═══════════════════════════════════════════╣"); 55 | 56 | // Print account info with proper formatting 57 | for line in account_info.lines() { 58 | println!("║ {:<42}║", line); 59 | } 60 | 61 | println!("╚═══════════════════════════════════════════╝\n"); 62 | } 63 | Err(e) => { 64 | eprintln!(" ❌ Failed to retrieve account information: {}", e); 65 | eprintln!(" Note: Make sure you're logged in to Claude CLI"); 66 | } 67 | } 68 | 69 | println!("3. Disconnecting..."); 70 | client.disconnect().await?; 71 | println!(" ✅ Disconnected\n"); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/e2e_hooks.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{ 2 | Query, HookCallback, HookContext, HookInput, HookJSONOutput, 3 | SyncHookJSONOutput, Result, SdkError, 4 | transport::mock::MockTransport, 5 | }; 6 | use async_trait::async_trait; 7 | use std::sync::Arc; 8 | use tokio::sync::Mutex; 9 | 10 | struct EchoHook; 11 | 12 | #[async_trait] 13 | impl HookCallback for EchoHook { 14 | async fn execute( 15 | &self, 16 | input: &HookInput, 17 | _tool_use_id: Option<&str>, 18 | _context: &HookContext, 19 | ) -> std::result::Result { 20 | // Echo the input back as additional context 21 | let input_json = serde_json::to_value(input) 22 | .unwrap_or_else(|_| serde_json::json!({})); 23 | 24 | Ok(HookJSONOutput::Sync(SyncHookJSONOutput { 25 | reason: Some(format!("Echoed input: {input_json}")), 26 | ..Default::default() 27 | })) 28 | } 29 | } 30 | 31 | #[tokio::test] 32 | async fn e2e_hook_callback_success() -> Result<()> { 33 | let (transport, mut handle) = MockTransport::pair(); 34 | let transport = Arc::new(Mutex::new(transport)); 35 | 36 | let mut q = Query::new(transport.clone(), false, None, None, std::collections::HashMap::new()); 37 | q.start().await?; 38 | 39 | // Register a known callback ID 40 | q.register_hook_callback_for_test("cb_test_1".to_string(), Arc::new(EchoHook)).await; 41 | 42 | // Send hook_callback control message from CLI -> SDK 43 | // Must use strongly-typed format with hook_event_name 44 | let req = serde_json::json!({ 45 | "type": "control_request", 46 | "request_id": "req_hook_1", 47 | "request": { 48 | "subtype": "hook_callback", 49 | "callbackId": "cb_test_1", 50 | "input": { 51 | "hook_event_name": "PreToolUse", 52 | "session_id": "test-session", 53 | "transcript_path": "/tmp/transcript", 54 | "cwd": "/test/dir", 55 | "tool_name": "TestTool", 56 | "tool_input": {"command": "test"} 57 | }, 58 | "toolUseId": "tu1" 59 | } 60 | }); 61 | handle.sdk_control_tx.send(req).await.unwrap(); 62 | 63 | // Expect a control_response from SDK -> CLI 64 | let outer = handle.outbound_control_rx.recv().await.unwrap(); 65 | assert_eq!(outer["type"], "control_response"); 66 | let resp = &outer["response"]; 67 | assert_eq!(resp["subtype"], "success"); 68 | assert_eq!(resp["request_id"], "req_hook_1"); 69 | 70 | // Verify the hook returned a reason with the echoed input 71 | let response = &resp["response"]; 72 | assert!(response.get("reason").is_some()); 73 | let reason = response["reason"].as_str().unwrap(); 74 | assert!(reason.contains("Echoed input")); 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/query_with_file_ops.rs: -------------------------------------------------------------------------------- 1 | //! Query with file operations example 2 | //! 3 | //! This example demonstrates how to use query() with BypassPermissions 4 | //! to allow file operations in --print mode. 5 | 6 | use cc_sdk::{ClaudeCodeOptions, Message, PermissionMode, Result, query}; 7 | use futures::StreamExt; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=info") 14 | .init(); 15 | 16 | println!("Claude Code SDK - Query with File Operations Example\n"); 17 | 18 | // Example: Query with file operations using BypassPermissions 19 | println!("Creating a file using query() with BypassPermissions"); 20 | println!("---------------------------------------------------"); 21 | println!("WARNING: BypassPermissions allows all operations without prompts!"); 22 | println!("Use this mode only in trusted environments.\n"); 23 | 24 | let options = ClaudeCodeOptions::builder() 25 | .system_prompt("You are a helpful coding assistant.") 26 | .model("sonnet") 27 | .permission_mode(PermissionMode::BypassPermissions) // Allow all operations 28 | .allowed_tools(vec!["write".to_string()]) // Still good practice to limit tools 29 | .build(); 30 | 31 | let mut messages = query( 32 | "Create a file called hello.txt with the content 'Hello from Rust SDK!'", 33 | Some(options), 34 | ) 35 | .await?; 36 | 37 | while let Some(msg) = messages.next().await { 38 | match msg? { 39 | Message::Assistant { message } => { 40 | for block in &message.content { 41 | match block { 42 | cc_sdk::ContentBlock::Text(text) => { 43 | println!("Claude: {}", text.text); 44 | } 45 | cc_sdk::ContentBlock::ToolUse(tool_use) => { 46 | println!("Claude is using tool: {} ({})", tool_use.name, tool_use.id); 47 | if let Some(file_path) = tool_use.input.get("file_path") { 48 | println!(" File path: {file_path}"); 49 | } 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | Message::Result { 56 | duration_ms, 57 | is_error, 58 | .. 59 | } => { 60 | if is_error { 61 | println!("\nQuery completed with error in {duration_ms}ms"); 62 | } else { 63 | println!("\nQuery completed successfully in {duration_ms}ms"); 64 | } 65 | break; 66 | } 67 | _ => {} 68 | } 69 | } 70 | 71 | println!("\nNote: For interactive permission prompts, use ClaudeSDKClient instead."); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_control_reception.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Result}; 2 | use futures::StreamExt; 3 | use std::sync::Arc; 4 | use tokio::sync::Mutex; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // Create a simple permission callback 9 | let permission_callback = Arc::new(TestPermissionCallback { 10 | log: Arc::new(Mutex::new(Vec::new())), 11 | }); 12 | 13 | let mut options = ClaudeCodeOptions::default(); 14 | options.can_use_tool = Some(permission_callback.clone()); 15 | 16 | let mut client = ClaudeSDKClient::new(options); 17 | 18 | println!("Testing control protocol reception..."); 19 | 20 | // Connect to CLI 21 | client.connect(Some("Test control protocol".to_string())).await?; 22 | 23 | // Send a test query that might trigger tool use 24 | client.send_user_message("Please use a tool to test permissions".to_string()).await?; 25 | 26 | // Receive messages 27 | let mut messages = client.receive_messages().await; 28 | let mut message_count = 0; 29 | 30 | while let Some(msg) = messages.next().await { 31 | message_count += 1; 32 | match msg { 33 | Ok(msg) => { 34 | println!("Received message: {msg:?}"); 35 | if matches!(msg, cc_sdk::Message::Result { .. }) { 36 | break; 37 | } 38 | } 39 | Err(e) => { 40 | eprintln!("Error: {e}"); 41 | break; 42 | } 43 | } 44 | 45 | if message_count > 10 { 46 | println!("Stopping after 10 messages"); 47 | break; 48 | } 49 | } 50 | 51 | // Check if permission callback was triggered 52 | let log = permission_callback.log.lock().await; 53 | if !log.is_empty() { 54 | println!("\n✅ Permission callback was triggered {} times!", log.len()); 55 | for entry in log.iter() { 56 | println!(" - {entry}"); 57 | } 58 | } else { 59 | println!("\n⚠️ Permission callback was not triggered"); 60 | } 61 | 62 | client.disconnect().await?; 63 | Ok(()) 64 | } 65 | 66 | struct TestPermissionCallback { 67 | log: Arc>>, 68 | } 69 | 70 | #[async_trait::async_trait] 71 | impl cc_sdk::CanUseTool for TestPermissionCallback { 72 | async fn can_use_tool( 73 | &self, 74 | tool_name: &str, 75 | _input: &serde_json::Value, 76 | _context: &cc_sdk::ToolPermissionContext, 77 | ) -> cc_sdk::PermissionResult { 78 | let mut log = self.log.lock().await; 79 | log.push(format!("Permission check for tool: {tool_name}")); 80 | println!("🔐 Permission callback triggered for tool: {}", tool_name); 81 | 82 | // Always allow for testing 83 | cc_sdk::PermissionResult::Allow(cc_sdk::PermissionResultAllow { 84 | updated_input: None, 85 | updated_permissions: None, 86 | }) 87 | } 88 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/e2e_set_model_and_end_input.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{Query, transport::mock::MockTransport}; 2 | use std::sync::Arc; 3 | use tokio::sync::Mutex; 4 | 5 | #[tokio::test] 6 | async fn e2e_set_model_sends_control_request() { 7 | let (transport, mut handle) = MockTransport::pair(); 8 | let transport = Arc::new(Mutex::new(transport)); 9 | 10 | let mut q = Query::new(transport.clone(), true, None, None, std::collections::HashMap::new()); 11 | q.start().await.unwrap(); 12 | 13 | // Spawn a task to mock the CLI response 14 | let sdk_control_tx = handle.sdk_control_tx.clone(); 15 | let responder = tokio::spawn(async move { 16 | // Wait for the outbound control request 17 | let req = handle.outbound_control_request_rx.recv().await.unwrap(); 18 | 19 | // Extract request_id and send back a success response 20 | let request_id = req.get("request_id") 21 | .and_then(|v| v.as_str()) 22 | .unwrap_or("unknown") 23 | .to_string(); 24 | 25 | let response = serde_json::json!({ 26 | "type": "control_response", 27 | "response": { 28 | "request_id": request_id, 29 | "subtype": "success", 30 | "response": {} 31 | } 32 | }); 33 | 34 | sdk_control_tx.send(response).await.unwrap(); 35 | req 36 | }); 37 | 38 | // Call set_model - it will now receive the mocked response 39 | let set_model = q.set_model(Some("sonnet".to_string())); 40 | 41 | // Wait for both the responder and set_model to complete 42 | let (req, result) = tokio::join!(responder, set_model); 43 | let req = req.unwrap(); 44 | result.unwrap(); 45 | 46 | // Assert the outbound control request was correct 47 | assert_eq!(req.get("type").and_then(|v| v.as_str()), Some("control_request")); 48 | assert_eq!( 49 | req.get("request").and_then(|r| r.get("type")).and_then(|v| v.as_str()), 50 | Some("set_model") 51 | ); 52 | assert_eq!( 53 | req.get("request").and_then(|r| r.get("model")).and_then(|v| v.as_str()), 54 | Some("sonnet") 55 | ); 56 | } 57 | 58 | #[tokio::test] 59 | async fn e2e_stream_input_calls_end_input() { 60 | let (transport, mut handle) = MockTransport::pair(); 61 | let transport = Arc::new(Mutex::new(transport)); 62 | 63 | let mut q = Query::new(transport.clone(), true, None, None, std::collections::HashMap::new()); 64 | q.start().await.unwrap(); 65 | 66 | // Prepare a short stream of input JSON values 67 | let inputs = vec![serde_json::json!("Hello"), serde_json::json!("World")]; 68 | let stream = futures::stream::iter(inputs); 69 | 70 | q.stream_input(stream).await.unwrap(); 71 | 72 | // Consume two input messages 73 | let _ = handle.sent_input_rx.recv().await.unwrap(); 74 | let _ = handle.sent_input_rx.recv().await.unwrap(); 75 | 76 | // Ensure end_input was called 77 | let ended = handle.end_input_rx.recv().await.unwrap(); 78 | assert!(ended); 79 | } 80 | 81 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_settings_safe.rs: -------------------------------------------------------------------------------- 1 | //! Safe example demonstrating the use of the settings parameter with proper file handling 2 | //! 3 | //! This example shows how to safely use a custom settings file with Claude Code 4 | 5 | use cc_sdk::{ClaudeCodeOptions, Result, query}; 6 | use futures::StreamExt; 7 | use std::path::Path; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=info") 14 | .init(); 15 | 16 | println!("Testing settings parameter with safe file handling...\n"); 17 | 18 | // Check if settings file exists and get the correct path 19 | let settings_path = if Path::new("examples/claude-settings.json").exists() { 20 | // Running from project root 21 | "examples/claude-settings.json" 22 | } else if Path::new("claude-settings.json").exists() { 23 | // Running from examples directory 24 | "claude-settings.json" 25 | } else { 26 | println!("Warning: Settings file not found, proceeding without it."); 27 | println!( 28 | "To use a settings file, ensure claude-settings.json exists in the current or examples directory.\n" 29 | ); 30 | // Use None for settings 31 | "" 32 | }; 33 | 34 | // Create options with a custom settings file (if it exists) 35 | let mut builder = ClaudeCodeOptions::builder() 36 | .system_prompt("You are a helpful assistant") 37 | .model("claude-3-opus-20240229") 38 | .permission_mode(cc_sdk::PermissionMode::AcceptEdits); 39 | 40 | if !settings_path.is_empty() { 41 | builder = builder.settings(settings_path); 42 | println!("Using settings file: {settings_path}"); 43 | } else { 44 | println!("Running without settings file"); 45 | } 46 | 47 | let options = builder.build(); 48 | 49 | println!("Querying Claude Code...\n"); 50 | 51 | // Make a simple query 52 | let mut messages = query( 53 | "What programming language is best for systems programming and why?", 54 | Some(options), 55 | ) 56 | .await?; 57 | 58 | // Process the response 59 | while let Some(msg) = messages.next().await { 60 | match msg? { 61 | cc_sdk::Message::Assistant { message } => { 62 | for block in message.content { 63 | if let cc_sdk::ContentBlock::Text(text) = block { 64 | println!("Claude: {}", text.text); 65 | } 66 | } 67 | } 68 | cc_sdk::Message::Result { 69 | duration_ms, 70 | total_cost_usd, 71 | .. 72 | } => { 73 | println!("\n---"); 74 | println!("Completed in {duration_ms}ms"); 75 | if let Some(cost) = total_cost_usd { 76 | println!("Cost: ${cost:.6}"); 77 | } 78 | } 79 | _ => {} 80 | } 81 | } 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_add_dirs.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating the use of add_dirs parameter 2 | //! 3 | //! This example shows how to add multiple directories as working directories 4 | 5 | use cc_sdk::{ClaudeCodeOptions, Result, query}; 6 | use futures::StreamExt; 7 | use std::path::PathBuf; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=info") 14 | .init(); 15 | 16 | println!("Testing add_dirs parameter...\n"); 17 | 18 | // Example 1: Using add_dir() to add directories one by one 19 | let options1 = ClaudeCodeOptions::builder() 20 | .cwd("/Users/zhangalex/Work/Projects/FW/rust-claude-code-api") 21 | .add_dir("/Users/zhangalex/Work/Projects/FW/claude-code-sdk-python") 22 | .add_dir("/Users/zhangalex/Work/Projects/FW/url-preview") 23 | .system_prompt("You have access to multiple project directories") 24 | .build(); 25 | 26 | println!("Example 1: Added directories one by one"); 27 | println!("Directories: {:?}\n", options1.add_dirs); 28 | 29 | // Example 2: Using add_dirs() to add multiple directories at once 30 | let dirs = vec![ 31 | PathBuf::from("/Users/zhangalex/Work/Projects/FW/rust-claude-code-api"), 32 | PathBuf::from("/Users/zhangalex/Work/Projects/FW/claude-code-sdk-python"), 33 | PathBuf::from("/Users/zhangalex/Work/Projects/FW/url-preview"), 34 | ]; 35 | 36 | let options2 = ClaudeCodeOptions::builder() 37 | .add_dirs(dirs.clone()) 38 | .system_prompt("You are working with multiple related projects") 39 | .permission_mode(cc_sdk::PermissionMode::AcceptEdits) 40 | .build(); 41 | 42 | println!("Example 2: Added directories in batch"); 43 | println!("Directories: {:?}\n", options2.add_dirs); 44 | 45 | // Make a query that could reference multiple directories 46 | println!("Querying Claude Code with access to multiple directories...\n"); 47 | 48 | let mut messages = query( 49 | "Can you list the main programming languages used across the directories I've given you access to?", 50 | Some(options2), 51 | ) 52 | .await?; 53 | 54 | // Process the response 55 | while let Some(msg) = messages.next().await { 56 | match msg? { 57 | cc_sdk::Message::Assistant { message } => { 58 | for block in message.content { 59 | if let cc_sdk::ContentBlock::Text(text) = block { 60 | println!("Claude: {}", text.text); 61 | } 62 | } 63 | } 64 | cc_sdk::Message::Result { 65 | duration_ms, 66 | total_cost_usd, 67 | .. 68 | } => { 69 | println!("\n---"); 70 | println!("Completed in {duration_ms}ms"); 71 | if let Some(cost) = total_cost_usd { 72 | println!("Cost: ${cost:.6}"); 73 | } 74 | } 75 | _ => {} 76 | } 77 | } 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/demo_project_naming.rs: -------------------------------------------------------------------------------- 1 | //! Demo: Project Naming Convention 2 | //! 3 | //! Shows how the project naming works: 4 | //! - Question set: qs00035.txt 5 | //! - Question 43 -> Project: q0003500043 6 | //! - Question 44 -> Project: q0003500044 7 | 8 | use regex::Regex; 9 | 10 | fn demonstrate_naming() { 11 | println!("🦀 Rust Question Processor - Project Naming Demo"); 12 | println!("{}", "=".repeat(50)); 13 | 14 | // Example question set files 15 | let question_sets = vec![ 16 | ("qs00001.txt", vec![1, 2, 3, 4, 5]), 17 | ("qs00002.txt", vec![1, 2, 3, 4, 5]), 18 | ("qs00035.txt", vec![43, 44, 45, 46, 47]), 19 | ]; 20 | 21 | for (qs_file, questions) in question_sets { 22 | println!("\nQuestion Set: {qs_file}"); 23 | 24 | // Extract qs number 25 | let qs_number = qs_file 26 | .strip_prefix("qs") 27 | .and_then(|s| s.strip_suffix(".txt")) 28 | .unwrap_or("00000"); 29 | 30 | println!(" QS Number: {qs_number}"); 31 | println!(" Projects:"); 32 | 33 | for &q_num in &questions { 34 | let project_name = format!("q{qs_number}{q_num:05}"); 35 | println!(" Question {q_num} → annotations/{project_name}"); 36 | } 37 | } 38 | 39 | println!("\n📁 Directory Structure:"); 40 | println!("```"); 41 | println!("."); 42 | println!("├── qs/"); 43 | println!("│ ├── qs00001.txt"); 44 | println!("│ ├── qs00002.txt"); 45 | println!("│ └── qs00035.txt"); 46 | println!("└── annotations/"); 47 | println!(" ├── q0000100001/ # From qs00001.txt, question 1"); 48 | println!(" ├── q0000100002/ # From qs00001.txt, question 2"); 49 | println!(" ├── q0003500043/ # From qs00035.txt, question 43"); 50 | println!(" └── q0003500044/ # From qs00035.txt, question 44"); 51 | println!("```"); 52 | 53 | // Show how to parse a question line 54 | println!("\n📝 Parsing Question Lines:"); 55 | let question_regex = Regex::new(r"^(\d+)\.\s*(.+)$").unwrap(); 56 | 57 | let sample_lines = vec![ 58 | "43. Implement a lock-free concurrent queue using atomic operations", 59 | "44. Create a procedural macro that derives a serialization trait", 60 | ]; 61 | 62 | for line in sample_lines { 63 | if let Some(captures) = question_regex.captures(line) { 64 | let q_num: u32 = captures[1].parse().unwrap(); 65 | let q_text = &captures[2]; 66 | println!("\n Line: {line}"); 67 | println!(" → Question Number: {q_num}"); 68 | println!(" → Question Text: {q_text}"); 69 | println!(" → Project Name (for qs00035): q00035{q_num:05}"); 70 | } 71 | } 72 | } 73 | 74 | fn main() { 75 | demonstrate_naming(); 76 | 77 | println!("\n✅ This naming convention ensures:"); 78 | println!(" - Unique project names across all question sets"); 79 | println!(" - Easy identification of source (qs file + question number)"); 80 | println!(" - Sorted order in file listings"); 81 | println!(" - Support for up to 99,999 question sets"); 82 | println!(" - Support for up to 99,999 questions per set"); 83 | } 84 | -------------------------------------------------------------------------------- /CONTROL_PROTOCOL_COMPATIBILITY.md: -------------------------------------------------------------------------------- 1 | # Control Protocol Compatibility Guide 2 | 3 | ## Overview 4 | 5 | The Rust SDK supports both legacy and new control protocol formats to ensure compatibility with different versions of the Claude Code CLI. 6 | 7 | ## Protocol Formats 8 | 9 | ### Legacy Format (Default) 10 | - **Request**: `{"type": "sdk_control_request", "request": {...}}` 11 | - **Response**: `{"type": "sdk_control_response", "response": {...}}` 12 | - **Compatibility**: All CLI versions 13 | - **Use Case**: Maximum compatibility with existing deployments 14 | 15 | ### New Format 16 | - **Request/Response**: `{"type": "control", "control": {...}}` 17 | - **Compatibility**: Newer CLI versions only 18 | - **Use Case**: Future-proof, unified protocol 19 | 20 | ## Configuration 21 | 22 | ### 1. Programmatic Configuration 23 | 24 | ```rust 25 | use cc_sdk::{ClaudeCodeOptions, ControlProtocolFormat}; 26 | 27 | let options = ClaudeCodeOptions::builder() 28 | .control_protocol_format(ControlProtocolFormat::Legacy) // Default 29 | // or ControlProtocolFormat::Control for new format 30 | // or ControlProtocolFormat::Auto for future auto-detection 31 | .build(); 32 | ``` 33 | 34 | ### 2. Environment Variable 35 | 36 | Set `CLAUDE_CODE_CONTROL_FORMAT` to override programmatic settings: 37 | 38 | ```bash 39 | export CLAUDE_CODE_CONTROL_FORMAT=legacy # Use legacy format 40 | # or 41 | export CLAUDE_CODE_CONTROL_FORMAT=control # Use new format 42 | ``` 43 | 44 | ## Receiving Messages 45 | 46 | The SDK **always** supports both formats when receiving messages (dual-stack): 47 | - Automatically detects and handles `type=control` 48 | - Also recognizes legacy `type=sdk_control_request` and `system sdk_control:*` variants 49 | 50 | ## Migration Strategy 51 | 52 | 1. **Current (v0.1.10+)**: Default to legacy format for maximum compatibility 53 | 2. **Check CLI Version**: Verify your CLI supports new format before switching 54 | 3. **Gradual Migration**: Use environment variable for testing without code changes 55 | 4. **Future**: Auto-detection will negotiate the best format automatically 56 | 57 | ## Compatibility Matrix 58 | 59 | | SDK Version | Default Format | Supports Legacy | Supports New | 60 | |-------------|----------------|-----------------|--------------| 61 | | v0.1.10+ | Legacy | ✅ Send/Receive | ✅ Receive, ⚠️ Send (opt-in) | 62 | | Future | Auto | ✅ Send/Receive | ✅ Send/Receive | 63 | 64 | ## Testing Your Configuration 65 | 66 | Run the included example to verify your setup: 67 | 68 | ```bash 69 | cargo run --example control_format_demo 70 | ``` 71 | 72 | ## Troubleshooting 73 | 74 | ### Symptoms of Format Mismatch 75 | - Permission callbacks not triggered 76 | - Hook callbacks not executed 77 | - MCP server messages not delivered 78 | - Control requests appear to be ignored 79 | 80 | ### Solution 81 | 1. Ensure you're using legacy format (default) 82 | 2. Or verify your CLI version supports new format before switching 83 | 3. Check logs for "Unknown message type" errors from CLI 84 | 85 | ## Python SDK Compatibility 86 | 87 | This implementation follows the same strategy as the Python SDK: 88 | - Default to legacy format for compatibility 89 | - Provide options for new format when CLI is ready 90 | - Environment variable override for flexibility -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/token_efficient.rs: -------------------------------------------------------------------------------- 1 | //! Token-efficient usage example 2 | //! 3 | //! Demonstrates best practices for minimizing token consumption and costs. 4 | 5 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, PermissionMode, Result}; 6 | use cc_sdk::model_recommendation::ModelRecommendation; 7 | use cc_sdk::token_tracker::{BudgetLimit, BudgetWarningCallback}; 8 | use std::sync::Arc; 9 | use futures::StreamExt; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | println!("=== Token-Efficient Claude Code Usage ===\n"); 14 | 15 | // Choose cost-effective model based on task 16 | let recommender = ModelRecommendation::default(); 17 | let model = recommender.suggest("simple").unwrap(); 18 | println!("📌 Using model: {model} (cheapest option)"); 19 | 20 | // Configure for minimal token usage 21 | let options = ClaudeCodeOptions::builder() 22 | .model(model) 23 | .max_turns(2) // Limit conversation length 24 | .max_output_tokens(1500) // Cap response size 25 | .allowed_tools(vec!["Read".to_string()]) // Only essential tools 26 | .permission_mode(PermissionMode::BypassPermissions) // Skip prompts 27 | .build(); 28 | 29 | let mut client = ClaudeSDKClient::new(options); 30 | 31 | // Set budget with warning callback 32 | println!("💰 Setting budget: $1.00 max\n"); 33 | let callback: BudgetWarningCallback = Arc::new(|msg: &str| eprintln!("⚠️ Budget Alert: {msg}")); 34 | client 35 | .set_budget_limit( 36 | BudgetLimit::with_cost(1.0).with_warning_threshold(0.8), 37 | Some(callback), 38 | ) 39 | .await; 40 | 41 | // Simple query 42 | println!("🔍 Query: What is 2+2?\n"); 43 | client.connect(Some("What is 2+2? Give a brief answer.".to_string())).await?; 44 | 45 | let mut messages = client.receive_messages().await; 46 | while let Some(msg) = messages.next().await { 47 | if let Ok(message) = msg { 48 | match message { 49 | cc_sdk::Message::Assistant { message } => { 50 | for block in &message.content { 51 | if let cc_sdk::ContentBlock::Text(text) = block { 52 | println!("💬 Response: {}", text.text); 53 | } 54 | } 55 | } 56 | cc_sdk::Message::Result { .. } => break, 57 | _ => {} 58 | } 59 | } 60 | } 61 | 62 | // Display usage stats 63 | let usage = client.get_usage_stats().await; 64 | println!("\n📊 Usage Statistics:"); 65 | println!(" Total tokens: {}", usage.total_tokens()); 66 | println!(" - Input: {} tokens", usage.total_input_tokens); 67 | println!(" - Output: {} tokens", usage.total_output_tokens); 68 | println!(" Cost: ${:.4}", usage.total_cost_usd); 69 | println!(" Sessions: {}", usage.session_count); 70 | 71 | if usage.session_count > 0 { 72 | println!(" Avg per session: {:.0} tokens", usage.avg_tokens_per_session()); 73 | } 74 | 75 | client.disconnect().await?; 76 | 77 | println!("\n✅ Demo complete!"); 78 | println!("💡 Compare this cost to using Opus without limits (~10-15x more expensive)"); 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/token_budget_monitoring.rs: -------------------------------------------------------------------------------- 1 | //! Token budget monitoring example 2 | //! 3 | //! Demonstrates comprehensive token usage tracking and budget management. 4 | 5 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Result}; 6 | use cc_sdk::token_tracker::{BudgetLimit, BudgetWarningCallback}; 7 | use futures::StreamExt; 8 | use std::sync::{Arc, Mutex}; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | println!("=== Token Budget Monitoring Demo ===\n"); 13 | 14 | // Track warnings 15 | let warnings = Arc::new(Mutex::new(Vec::new())); 16 | let warnings_clone = warnings.clone(); 17 | 18 | let options = ClaudeCodeOptions::builder() 19 | .model("claude-3-5-haiku-20241022") 20 | .max_turns(3) 21 | .build(); 22 | 23 | let mut client = ClaudeSDKClient::new(options); 24 | 25 | // Set budget with callback 26 | println!("Setting budget: $0.50 max (warning at 80%)"); 27 | let cb: BudgetWarningCallback = Arc::new(move |msg: &str| { 28 | println!("⚠️ {msg}"); 29 | warnings_clone.lock().unwrap().push(msg.to_string()); 30 | }); 31 | client 32 | .set_budget_limit( 33 | BudgetLimit::with_cost(0.50).with_warning_threshold(0.8), 34 | Some(cb), 35 | ) 36 | .await; 37 | 38 | // Run multiple queries to demonstrate tracking 39 | let queries = ["What is 2+2?", 40 | "What is the capital of France?", 41 | "Explain quantum computing in one sentence."]; 42 | 43 | for (i, query) in queries.iter().enumerate() { 44 | println!("\n--- Query {} ---", i + 1); 45 | println!("Question: {query}"); 46 | 47 | client.connect(Some(query.to_string())).await?; 48 | 49 | let mut messages = client.receive_messages().await; 50 | while let Some(msg) = messages.next().await { 51 | if let Ok(message) = msg 52 | && let cc_sdk::Message::Result { .. } = message { 53 | break; 54 | } 55 | } 56 | 57 | client.disconnect().await?; 58 | 59 | // Check usage after each query 60 | let usage = client.get_usage_stats().await; 61 | println!("Cumulative usage: {} tokens, ${:.4}", 62 | usage.total_tokens(), usage.total_cost_usd); 63 | 64 | // Check budget status 65 | if client.is_budget_exceeded().await { 66 | println!("❌ Budget exceeded! Stopping."); 67 | break; 68 | } 69 | } 70 | 71 | // Final report 72 | let usage = client.get_usage_stats().await; 73 | println!("\n=== Final Report ==="); 74 | println!("Total queries: {}", usage.session_count); 75 | println!("Total tokens: {}", usage.total_tokens()); 76 | println!(" Input: {} tokens", usage.total_input_tokens); 77 | println!(" Output: {} tokens", usage.total_output_tokens); 78 | println!("Total cost: ${:.4}", usage.total_cost_usd); 79 | println!("Average per query: {:.0} tokens", usage.avg_tokens_per_session()); 80 | 81 | let warnings = warnings.lock().unwrap(); 82 | if !warnings.is_empty() { 83 | println!("\n⚠️ Warnings triggered: {}", warnings.len()); 84 | for warning in warnings.iter() { 85 | println!(" - {warning}"); 86 | } 87 | } 88 | 89 | println!("\n✅ Monitoring demo complete!"); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /claude-code-api/src/models/error.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::StatusCode, 3 | response::{IntoResponse, Response}, 4 | Json, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Debug, thiserror::Error)] 9 | #[allow(dead_code)] 10 | pub enum ApiError { 11 | #[error("Internal server error: {0}")] 12 | Internal(String), 13 | 14 | #[error("Bad request: {0}")] 15 | BadRequest(String), 16 | 17 | #[error("Unauthorized: {0}")] 18 | Unauthorized(String), 19 | 20 | #[error("Not found: {0}")] 21 | NotFound(String), 22 | 23 | #[error("Claude process error: {0}")] 24 | ClaudeProcess(String), 25 | 26 | #[error("Database error: {0}")] 27 | Database(#[from] sqlx::Error), 28 | 29 | #[error("Configuration error: {0}")] 30 | Config(#[from] config::ConfigError), 31 | 32 | #[error("IO error: {0}")] 33 | Io(#[from] std::io::Error), 34 | 35 | #[error("JSON error: {0}")] 36 | Json(#[from] serde_json::Error), 37 | 38 | #[error("Timeout error: {0}")] 39 | Timeout(String), 40 | 41 | #[error("Rate limit exceeded: {0}")] 42 | RateLimit(String), 43 | 44 | #[error("Service unavailable: {0}")] 45 | ServiceUnavailable(String), 46 | 47 | #[error("Invalid model: {0}")] 48 | InvalidModel(String), 49 | 50 | #[error("Context length exceeded: {0}")] 51 | ContextLengthExceeded(String), 52 | } 53 | 54 | #[derive(Debug, Serialize, Deserialize)] 55 | pub struct ErrorResponse { 56 | pub error: ErrorDetail, 57 | } 58 | 59 | #[derive(Debug, Serialize, Deserialize)] 60 | pub struct ErrorDetail { 61 | pub message: String, 62 | pub r#type: String, 63 | pub param: Option, 64 | pub code: Option, 65 | } 66 | 67 | impl IntoResponse for ApiError { 68 | fn into_response(self) -> Response { 69 | let (status, error_type, code) = match &self { 70 | ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, "invalid_request_error", None), 71 | ApiError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "authentication_error", None), 72 | ApiError::NotFound(_) => (StatusCode::NOT_FOUND, "not_found_error", None), 73 | ApiError::ClaudeProcess(_) => (StatusCode::INTERNAL_SERVER_ERROR, "claude_process_error", None), 74 | ApiError::Timeout(_) => (StatusCode::GATEWAY_TIMEOUT, "timeout_error", Some("timeout")), 75 | ApiError::RateLimit(_) => (StatusCode::TOO_MANY_REQUESTS, "rate_limit_error", Some("rate_limit_exceeded")), 76 | ApiError::ServiceUnavailable(_) => (StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", None), 77 | ApiError::InvalidModel(_) => (StatusCode::BAD_REQUEST, "invalid_request_error", Some("invalid_model")), 78 | ApiError::ContextLengthExceeded(_) => (StatusCode::BAD_REQUEST, "invalid_request_error", Some("context_length_exceeded")), 79 | _ => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error", None), 80 | }; 81 | 82 | let error_response = ErrorResponse { 83 | error: ErrorDetail { 84 | message: self.to_string(), 85 | r#type: error_type.to_string(), 86 | param: None, 87 | code: code.map(String::from), 88 | }, 89 | }; 90 | 91 | (status, Json(error_response)).into_response() 92 | } 93 | } 94 | 95 | pub type ApiResult = Result; -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/performance_comparison.rs: -------------------------------------------------------------------------------- 1 | //! Performance comparison between normal and optimized clients 2 | 3 | use cc_sdk::{ 4 | ClaudeCodeOptions, ClientMode, InteractiveClient, OptimizedClient, PermissionMode, Result, 5 | }; 6 | use std::time::Instant; 7 | use tracing::{Level, info}; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt().with_max_level(Level::INFO).init(); 12 | 13 | info!("=== Performance Comparison: Normal vs Optimized ===\n"); 14 | 15 | let options = ClaudeCodeOptions::builder() 16 | .permission_mode(PermissionMode::AcceptEdits) 17 | .build(); 18 | 19 | // Test queries 20 | let queries = ["What is 10 + 10?", "What is 20 + 20?", "What is 30 + 30?"]; 21 | 22 | // Test 1: Traditional InteractiveClient (creates new process each time) 23 | info!("Test 1: Traditional InteractiveClient"); 24 | info!("(Creates new claude-code process for each connection)\n"); 25 | 26 | let mut traditional_times = Vec::new(); 27 | for (i, query) in queries.iter().enumerate() { 28 | let start = Instant::now(); 29 | 30 | let mut client = InteractiveClient::new(options.clone())?; 31 | client.connect().await?; 32 | let _ = client.send_and_receive(query.to_string()).await?; 33 | client.disconnect().await?; 34 | 35 | let elapsed = start.elapsed(); 36 | traditional_times.push(elapsed); 37 | info!("Query {}: {:?}", i + 1, elapsed); 38 | } 39 | 40 | let traditional_avg = 41 | traditional_times.iter().sum::() / traditional_times.len() as u32; 42 | info!("Average time: {:?}\n", traditional_avg); 43 | 44 | // Test 2: OptimizedClient with connection pooling 45 | info!("Test 2: OptimizedClient with Connection Pooling"); 46 | info!("(Reuses claude-code processes, pre-warmed)\n"); 47 | 48 | let optimized_client = OptimizedClient::new(options, ClientMode::OneShot)?; 49 | 50 | // Pre-warm the connection pool 51 | info!("Pre-warming connection pool..."); 52 | let warmup_start = Instant::now(); 53 | let _ = optimized_client.query("Hi".to_string()).await?; 54 | info!("Pool warmed up in {:?}\n", warmup_start.elapsed()); 55 | 56 | let mut optimized_times = Vec::new(); 57 | for (i, query) in queries.iter().enumerate() { 58 | let start = Instant::now(); 59 | let _ = optimized_client.query(query.to_string()).await?; 60 | let elapsed = start.elapsed(); 61 | optimized_times.push(elapsed); 62 | info!("Query {}: {:?}", i + 1, elapsed); 63 | } 64 | 65 | let optimized_avg = 66 | optimized_times.iter().sum::() / optimized_times.len() as u32; 67 | info!("Average time: {:?}\n", optimized_avg); 68 | 69 | // Summary 70 | info!("=== Summary ==="); 71 | info!("Traditional average: {:?}", traditional_avg); 72 | info!("Optimized average: {:?}", optimized_avg); 73 | 74 | let improvement = (traditional_avg.as_millis() as f64 - optimized_avg.as_millis() as f64) 75 | / traditional_avg.as_millis() as f64 76 | * 100.0; 77 | info!("Performance improvement: {:.1}%", improvement); 78 | 79 | info!("\nKey optimizations:"); 80 | info!("✓ Connection pooling - reuses claude-code processes"); 81 | info!("✓ Pre-warming - first connection established early"); 82 | info!("✓ Reduced overhead - no process creation per query"); 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /claude-code-api/src/api/conversations.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Path, State}, 3 | response::IntoResponse, 4 | Json, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | use std::sync::Arc; 8 | use chrono::{DateTime, Utc}; 9 | 10 | use crate::{ 11 | core::conversation::ConversationManager, 12 | models::error::{ApiError, ApiResult}, 13 | }; 14 | 15 | #[derive(Clone)] 16 | pub struct ConversationState { 17 | pub manager: Arc, 18 | } 19 | 20 | #[derive(Debug, Serialize)] 21 | pub struct ConversationResponse { 22 | pub id: String, 23 | pub created_at: DateTime, 24 | pub updated_at: DateTime, 25 | pub message_count: usize, 26 | pub metadata: serde_json::Value, 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | pub struct CreateConversationRequest { 31 | pub model: Option, 32 | pub project_path: Option, 33 | } 34 | 35 | pub async fn create_conversation( 36 | State(state): State, 37 | Json(request): Json, 38 | ) -> ApiResult { 39 | let id = state.manager.create_conversation(request.model.clone()); 40 | 41 | if let Some(project_path) = request.project_path { 42 | state.manager.update_metadata(&id, |metadata| { 43 | metadata.project_path = Some(project_path); 44 | }).map_err(|e| ApiError::Internal(e.to_string()))?; 45 | } 46 | 47 | let conversation = state.manager.get_conversation(&id) 48 | .ok_or_else(|| ApiError::Internal("Failed to retrieve created conversation".to_string()))?; 49 | 50 | let response = ConversationResponse { 51 | id: conversation.id, 52 | created_at: conversation.created_at, 53 | updated_at: conversation.updated_at, 54 | message_count: conversation.messages.len(), 55 | metadata: serde_json::to_value(conversation.metadata)?, 56 | }; 57 | 58 | Ok(Json(response)) 59 | } 60 | 61 | pub async fn get_conversation( 62 | State(state): State, 63 | Path(conversation_id): Path, 64 | ) -> ApiResult { 65 | let conversation = state.manager.get_conversation(&conversation_id) 66 | .ok_or_else(|| ApiError::NotFound("Conversation not found".to_string()))?; 67 | 68 | let response = ConversationResponse { 69 | id: conversation.id, 70 | created_at: conversation.created_at, 71 | updated_at: conversation.updated_at, 72 | message_count: conversation.messages.len(), 73 | metadata: serde_json::to_value(conversation.metadata)?, 74 | }; 75 | 76 | Ok(Json(response)) 77 | } 78 | 79 | #[derive(Debug, Serialize)] 80 | pub struct ConversationListResponse { 81 | pub conversations: Vec, 82 | } 83 | 84 | #[derive(Debug, Serialize)] 85 | pub struct ConversationSummary { 86 | pub id: String, 87 | pub updated_at: DateTime, 88 | } 89 | 90 | pub async fn list_conversations( 91 | State(state): State, 92 | ) -> ApiResult { 93 | let conversations = state.manager.list_active_conversations(); 94 | 95 | let response = ConversationListResponse { 96 | conversations: conversations.into_iter() 97 | .map(|(id, updated_at)| ConversationSummary { id, updated_at }) 98 | .collect(), 99 | }; 100 | 101 | Ok(Json(response)) 102 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_concurrent_leak.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::{query, Result}; 2 | use futures::StreamExt; 3 | use std::process::Command; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | println!("Testing concurrent process leak fix...\n"); 8 | 9 | // Check initial Claude processes 10 | let initial_count = count_claude_processes(); 11 | println!("Initial Claude processes: {initial_count}"); 12 | 13 | println!("\n--- Running 5 concurrent queries ---"); 14 | 15 | // Create 5 concurrent queries 16 | let mut handles = vec![]; 17 | for i in 1..=5 { 18 | let handle = tokio::spawn(async move { 19 | println!("Starting query {i}"); 20 | 21 | // Create a query and consume only the first message 22 | let mut messages = query(format!("Say 'Concurrent test {i}'"), None).await?; 23 | 24 | // Only take the first message then drop the stream 25 | if let Some(msg) = messages.next().await { 26 | match msg { 27 | Ok(_) => println!("Query {i} got response"), 28 | Err(e) => println!("Query {i} error: {e}"), 29 | } 30 | } 31 | 32 | // Stream is dropped here, should trigger cleanup 33 | drop(messages); 34 | 35 | println!("Query {i} completed"); 36 | Ok::<(), cc_sdk::SdkError>(()) 37 | }); 38 | handles.push(handle); 39 | } 40 | 41 | // Wait for all queries to complete 42 | for handle in handles { 43 | let _ = handle.await; 44 | } 45 | 46 | // Give some time for cleanup 47 | tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 48 | 49 | let concurrent_count = count_claude_processes(); 50 | println!("\nClaude processes after concurrent queries: {concurrent_count}"); 51 | 52 | if concurrent_count > initial_count + 1 { 53 | println!("⚠️ WARNING: Possible process leak! Expected at most {} processes, found {}", 54 | initial_count + 1, concurrent_count); 55 | } 56 | 57 | // Final check after more delay 58 | println!("\n--- Final check after 3 more seconds ---"); 59 | tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; 60 | 61 | let final_count = count_claude_processes(); 62 | println!("Final Claude processes: {final_count}"); 63 | 64 | if final_count > initial_count { 65 | println!("❌ FAILED: Process leak detected! {} zombie processes remain", 66 | final_count - initial_count); 67 | 68 | // Show which processes are still running 69 | let output = Command::new("sh") 70 | .arg("-c") 71 | .arg("ps aux | grep -v grep | grep claude") 72 | .output() 73 | .expect("Failed to execute ps command"); 74 | println!("\nRemaining Claude processes:"); 75 | println!("{}", String::from_utf8_lossy(&output.stdout)); 76 | } else { 77 | println!("✅ SUCCESS: No process leak detected in concurrent scenario!"); 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | fn count_claude_processes() -> usize { 84 | let output = Command::new("sh") 85 | .arg("-c") 86 | .arg("ps aux | grep -v grep | grep claude | wc -l") 87 | .output() 88 | .expect("Failed to execute ps command"); 89 | 90 | let count_str = String::from_utf8_lossy(&output.stdout); 91 | count_str.trim().parse().unwrap_or(0) 92 | } -------------------------------------------------------------------------------- /docs/url-preview-integration.md: -------------------------------------------------------------------------------- 1 | # url-preview Integration Guide 2 | 3 | This guide explains how to integrate claude-code-api with the url-preview library. 4 | 5 | ## Overview 6 | 7 | url-preview expects OpenAI-compatible API responses with `tool_calls` format (not the legacy `function_call` format). claude-code-api has been updated to support this. 8 | 9 | ## Key Issues and Solutions 10 | 11 | ### 1. Invalid Model Names 12 | 13 | ❌ **Problem**: url-preview examples use `claude-3-opus-20240229` which is invalid for Claude CLI. 14 | 15 | ✅ **Solution**: Use valid model names: 16 | - `claude-3-5-haiku-20241022` (fastest) 17 | - `claude-3-5-sonnet-20241022` (balanced) 18 | - `opus` (most capable, alias for latest Opus) 19 | - `claude-opus-4-20250514` (specific version) 20 | 21 | ### 2. Function Call Format 22 | 23 | ❌ **Problem**: url-preview expects `tool_calls` array, not `function_call` object. 24 | 25 | ✅ **Solution**: claude-code-api now detects when `tools` are used (vs `functions`) and returns the correct format: 26 | 27 | ```json 28 | { 29 | "choices": [{ 30 | "message": { 31 | "tool_calls": [{ 32 | "id": "call_xxx", 33 | "type": "function", 34 | "function": { 35 | "name": "extract_data", 36 | "arguments": "{...}" 37 | } 38 | }] 39 | } 40 | }] 41 | } 42 | ``` 43 | 44 | ## Working Example 45 | 46 | ```rust 47 | use url_preview::{LLMExtractor, LLMExtractorConfig, OpenAIProvider, Fetcher}; 48 | use std::sync::Arc; 49 | 50 | // Configure for claude-code-api 51 | let config = async_openai::config::OpenAIConfig::new() 52 | .with_api_base("http://localhost:8080/v1") 53 | .with_api_key("not-needed"); 54 | 55 | // Use a VALID model name 56 | let provider = Arc::new( 57 | OpenAIProvider::from_config(config, "claude-3-5-haiku-20241022".to_string()) 58 | ); 59 | 60 | // Extract data 61 | let extractor = LLMExtractor::new(provider); 62 | let fetcher = Fetcher::new(); 63 | let result = extractor.extract::(url, &fetcher).await?; 64 | ``` 65 | 66 | ## Running the Integration 67 | 68 | 1. Start claude-code-api: 69 | ```bash 70 | RUST_LOG=info cargo run --bin ccapi 71 | ``` 72 | 73 | 2. In url-preview project, update the model name in examples: 74 | ```rust 75 | // Change from: 76 | "claude-3-opus-20240229" 77 | // To: 78 | "opus" // or "claude-3-5-sonnet-20241022" 79 | ``` 80 | 81 | 3. Run url-preview example: 82 | ```bash 83 | cargo run --example claude_api_working --features llm 84 | ``` 85 | 86 | ## Troubleshooting 87 | 88 | ### "Invalid model name" Error 89 | - Check the model name against the valid list above 90 | - Use `opus` or `sonnet` aliases for simplicity 91 | 92 | ### "No function call or valid JSON in response" Error 93 | - Make sure you're using the latest claude-code-api with tool_calls support 94 | - Verify the API is returning tool_calls when tools are requested 95 | 96 | ### Timeout Errors 97 | - Increase timeout in .env: `CLAUDE_CODE__CLAUDE__TIMEOUT_SECONDS=600` 98 | - Use simpler schemas or smaller content 99 | 100 | ## API Request Format 101 | 102 | url-preview sends requests like this: 103 | ```json 104 | { 105 | "model": "claude-3-5-haiku-20241022", 106 | "messages": [...], 107 | "tools": [{ 108 | "type": "function", 109 | "function": { 110 | "name": "extract_data", 111 | "description": "...", 112 | "parameters": {...} 113 | } 114 | }], 115 | "tool_choice": "required" 116 | } 117 | ``` 118 | 119 | claude-code-api will: 120 | 1. Detect the `tools` array (not `functions`) 121 | 2. Process Claude's JSON response 122 | 3. Return it in `tool_calls` format (not `function_call`) 123 | 124 | This ensures compatibility with url-preview's OpenAI provider implementation. -------------------------------------------------------------------------------- /claude-code-api/src/core/config.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, ConfigError, Environment, File}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::env; 4 | 5 | #[derive(Debug, Deserialize, Serialize, Clone)] 6 | pub struct Settings { 7 | pub server: ServerConfig, 8 | pub claude: ClaudeConfig, 9 | pub database: DatabaseConfig, 10 | pub auth: AuthConfig, 11 | #[serde(default)] 12 | pub file_access: FileAccessConfig, 13 | #[serde(default)] 14 | pub mcp: MCPConfig, 15 | #[serde(default)] 16 | pub process_pool: ProcessPoolConfig, 17 | } 18 | 19 | #[derive(Debug, Deserialize, Serialize, Clone)] 20 | pub struct ServerConfig { 21 | pub host: String, 22 | pub port: u16, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Serialize, Clone)] 26 | pub struct ClaudeConfig { 27 | pub command: String, 28 | pub timeout_seconds: u64, 29 | pub max_concurrent_sessions: usize, 30 | #[serde(default)] 31 | pub use_interactive_sessions: bool, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Serialize, Clone)] 35 | pub struct DatabaseConfig { 36 | pub url: String, 37 | pub max_connections: u32, 38 | } 39 | 40 | #[derive(Debug, Deserialize, Serialize, Clone)] 41 | pub struct AuthConfig { 42 | pub enabled: bool, 43 | pub secret_key: String, 44 | pub token_expiry_hours: i64, 45 | } 46 | 47 | #[derive(Debug, Deserialize, Serialize, Clone)] 48 | #[derive(Default)] 49 | pub struct FileAccessConfig { 50 | pub skip_permissions: bool, 51 | pub additional_dirs: Vec, 52 | } 53 | 54 | 55 | #[derive(Debug, Deserialize, Serialize, Clone)] 56 | #[derive(Default)] 57 | pub struct MCPConfig { 58 | pub enabled: bool, 59 | pub config_file: Option, 60 | pub config_json: Option, 61 | pub strict: bool, 62 | pub debug: bool, 63 | } 64 | 65 | #[derive(Debug, Deserialize, Serialize, Clone)] 66 | pub struct ProcessPoolConfig { 67 | pub size: usize, 68 | pub min_idle: usize, 69 | pub max_idle: usize, 70 | } 71 | 72 | impl Default for ProcessPoolConfig { 73 | fn default() -> Self { 74 | Self { 75 | size: 5, 76 | min_idle: 2, 77 | max_idle: 5, 78 | } 79 | } 80 | } 81 | 82 | 83 | impl Settings { 84 | pub fn new() -> Result { 85 | let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); 86 | 87 | let s = Config::builder() 88 | .set_default("server.host", "0.0.0.0")? 89 | .set_default("server.port", 8080)? 90 | .set_default("claude.command", "claude")? 91 | .set_default("claude.timeout_seconds", 300)? 92 | .set_default("claude.max_concurrent_sessions", 10)? 93 | .set_default("claude.use_interactive_sessions", false)? 94 | .set_default("database.url", "sqlite://./claude_code.db")? 95 | .set_default("database.max_connections", 5)? 96 | .set_default("auth.enabled", false)? 97 | .set_default("auth.secret_key", "change-me-in-production")? 98 | .set_default("auth.token_expiry_hours", 24)? 99 | .set_default("file_access.skip_permissions", false)? 100 | .set_default("file_access.additional_dirs", Vec::::new())? 101 | .set_default("mcp.enabled", false)? 102 | .set_default("mcp.strict", false)? 103 | .set_default("mcp.debug", false)? 104 | .add_source(File::with_name(&format!("config/{run_mode}")).required(false)) 105 | .add_source(File::with_name("config/local").required(false)) 106 | .add_source(Environment::with_prefix("CLAUDE_CODE").separator("__")) 107 | .build()?; 108 | 109 | s.try_deserialize() 110 | } 111 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/simple_model_test.rs: -------------------------------------------------------------------------------- 1 | //! Simple test for model availability 2 | //! Run with: cargo run --example simple_model_test 3 | 4 | use cc_sdk::{query, ClaudeCodeOptions, PermissionMode, Result}; 5 | use futures::StreamExt; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | println!("=== Testing Model and Plan Mode ===\n"); 10 | 11 | // Test 1: Use working model with Plan mode 12 | println!("Test 1: Using 'sonnet' alias with Plan mode"); 13 | let options = ClaudeCodeOptions::builder() 14 | .model("sonnet") // This works based on previous test 15 | .permission_mode(PermissionMode::Plan) 16 | .max_turns(1) 17 | .build(); 18 | 19 | match query("Say 'Plan mode works'", Some(options)).await { 20 | Ok(mut stream) => { 21 | let mut success = false; 22 | while let Some(msg) = stream.next().await { 23 | if let Ok(cc_sdk::Message::Assistant { message }) = msg { 24 | for block in message.content { 25 | if let cc_sdk::ContentBlock::Text(text) = block { 26 | println!("Response: {}", text.text); 27 | success = true; 28 | } 29 | } 30 | } 31 | } 32 | if success { 33 | println!("✅ Plan mode with 'sonnet' works!\n"); 34 | } 35 | } 36 | Err(e) => println!("❌ Error: {e:?}\n"), 37 | } 38 | 39 | // Test 2: Use full model name 40 | println!("Test 2: Using full Opus 4.1 name"); 41 | let options = ClaudeCodeOptions::builder() 42 | .model("claude-opus-4-1-20250805") 43 | .max_turns(1) 44 | .build(); 45 | 46 | match query("What model are you?", Some(options)).await { 47 | Ok(mut stream) => { 48 | let mut success = false; 49 | while let Some(msg) = stream.next().await { 50 | if let Ok(cc_sdk::Message::Assistant { message }) = msg { 51 | for block in message.content { 52 | if let cc_sdk::ContentBlock::Text(text) = block { 53 | let preview = if text.text.len() > 100 { 54 | format!("{}...", &text.text[..100]) 55 | } else { 56 | text.text.clone() 57 | }; 58 | println!("Response: {preview}"); 59 | success = true; 60 | } 61 | } 62 | } 63 | } 64 | if success { 65 | println!("✅ Full Opus 4.1 name works!\n"); 66 | } 67 | } 68 | Err(e) => println!("❌ Error: {e:?}\n"), 69 | } 70 | 71 | // Test 3: Working model names summary 72 | println!("=== Working Model Names ==="); 73 | println!("✅ Aliases that work:"); 74 | println!(" - 'opus' (maps to latest Opus)"); 75 | println!(" - 'sonnet' (maps to latest Sonnet)"); 76 | println!("\n✅ Full names that work:"); 77 | println!(" - 'claude-opus-4-1-20250805' (Opus 4.1)"); 78 | println!(" - 'claude-sonnet-4-20250514' (Sonnet 4)"); 79 | println!(" - 'claude-3-5-sonnet-20241022' (Claude 3.5 Sonnet)"); 80 | println!(" - 'claude-3-5-haiku-20241022' (Claude 3.5 Haiku)"); 81 | println!("\n❌ Names that DON'T work:"); 82 | println!(" - 'opus-4.1' (returns 404)"); 83 | println!(" - 'sonnet-4' (returns 404)"); 84 | println!("\n✅ Permission modes supported:"); 85 | println!(" - PermissionMode::Default"); 86 | println!(" - PermissionMode::AcceptEdits"); 87 | println!(" - PermissionMode::Plan (new in v0.1.7)"); 88 | println!(" - PermissionMode::BypassPermissions"); 89 | 90 | Ok(()) 91 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/session_with_account_info.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating interactive session with automatic account info display 2 | //! 3 | //! This example shows how to start a session and automatically display 4 | //! account information at the beginning for verification purposes. 5 | 6 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Message, Result}; 7 | use futures::StreamExt; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | println!("╔═══════════════════════════════════════════════╗"); 12 | println!("║ Interactive Session with Account Info ║"); 13 | println!("╚═══════════════════════════════════════════════╝\n"); 14 | 15 | // Configure options 16 | let options = ClaudeCodeOptions::builder() 17 | .model("claude-sonnet-4-5-20250929") // Use latest Sonnet 18 | .system_prompt("You are a helpful coding assistant") 19 | .build(); 20 | 21 | let mut client = ClaudeSDKClient::new(options); 22 | 23 | // Step 1: Connect 24 | println!("🔌 Connecting to Claude CLI..."); 25 | client.connect(None).await?; 26 | println!(" ✅ Connected\n"); 27 | 28 | // Step 2: Display account information 29 | println!("👤 Fetching account information..."); 30 | match client.get_account_info().await { 31 | Ok(account_info) => { 32 | println!("╭─────────────────────────────────────────────╮"); 33 | println!("│ 📋 Current Session Account │"); 34 | println!("├─────────────────────────────────────────────┤"); 35 | for line in account_info.lines() { 36 | println!("│ {:<44}│", line); 37 | } 38 | println!("╰─────────────────────────────────────────────╯\n"); 39 | } 40 | Err(e) => { 41 | eprintln!("⚠️ Warning: Could not retrieve account info: {}\n", e); 42 | } 43 | } 44 | 45 | // Step 3: Continue with normal session 46 | println!("💬 Starting interactive session...\n"); 47 | println!("─────────────────────────────────────────────────\n"); 48 | 49 | // Send a query 50 | client.send_user_message("What is the capital of France?".to_string()).await?; 51 | 52 | // Receive response 53 | let mut messages = client.receive_messages().await; 54 | while let Some(msg_result) = messages.next().await { 55 | match msg_result? { 56 | Message::Assistant { message } => { 57 | for block in message.content { 58 | if let cc_sdk::ContentBlock::Text(text) = block { 59 | println!("🤖 Claude: {}\n", text.text); 60 | } 61 | } 62 | } 63 | Message::Result { duration_ms, usage, total_cost_usd, .. } => { 64 | println!("─────────────────────────────────────────────────"); 65 | println!("📊 Response Stats:"); 66 | println!(" ⏱️ Duration: {}ms", duration_ms); 67 | 68 | if let Some(usage_json) = usage { 69 | if let Some(input_tokens) = usage_json.get("input_tokens") { 70 | println!(" 📥 Input tokens: {}", input_tokens); 71 | } 72 | if let Some(output_tokens) = usage_json.get("output_tokens") { 73 | println!(" 📤 Output tokens: {}", output_tokens); 74 | } 75 | } 76 | 77 | if let Some(cost) = total_cost_usd { 78 | println!(" 💰 Cost: ${:.6}", cost); 79 | } 80 | println!("─────────────────────────────────────────────────\n"); 81 | break; 82 | } 83 | _ => {} 84 | } 85 | } 86 | 87 | // Disconnect 88 | println!("🔌 Disconnecting..."); 89 | client.disconnect().await?; 90 | println!(" ✅ Session ended\n"); 91 | 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Module Organization 4 | - Workspace crates: `claude-code-api/` (server) and `claude-code-sdk-rs/` (SDK). 5 | - API crate modules: `src/api/`, `src/core/`, `src/models/`, `src/utils/`, `src/middleware/`. 6 | - Shared assets/docs: `docs/`, `doc/`, `examples/`, `migrations/`, `config/`, `script/`. 7 | - Integration tests live in `tests/`; unit tests live alongside modules in `src/...`. 8 | 9 | ## Architecture Overview 10 | - Server: Axum + Tokio; routes in `src/api/*` call services in `src/core/*`. 11 | - Process pool: `core/process_pool.rs` manages Claude CLI processes; interactive mode in `core/interactive_session.rs`. 12 | - SDK: `claude-code-sdk-rs` provides CLI orchestration and streaming; API composes it. 13 | - Middleware: request ID and error mapping in `src/middleware/*`; caching and conversations under `core/*`. 14 | 15 | ## Build, Test, and Development Commands 16 | - Build all: `cargo build` (use `--release` for production). 17 | - Run API server: `RUST_LOG=info cargo run -p claude-code-api --bin claude-code-api` (alias: `--bin ccapi`). 18 | - Quick start script: `script/run.sh` (builds release and runs). 19 | - Test all crates: `cargo test` (or per crate: `cargo test -p claude-code-api`). 20 | - Lint/format: `cargo fmt --all` and `cargo clippy --all-targets --all-features -D warnings`. 21 | - Docker: `docker build -t claude-code-api . && docker run --rm -p 8080:8080 --env-file .env claude-code-api`. 22 | 23 | ## Coding Style & Naming Conventions 24 | - Rust edition: 2024; follow `rustfmt` defaults (4‑space indent, max width default). 25 | - Naming: modules/files `snake_case`, types `PascalCase`, functions/vars `snake_case`, constants `SCREAMING_SNAKE_CASE`. 26 | - Errors: prefer `anyhow::Result` at app edges; library code defines errors with `thiserror`. 27 | - Logging: use `tracing` with `RUST_LOG` (e.g., `RUST_LOG=info`). 28 | 29 | ## Testing Guidelines 30 | - Frameworks: `tokio::test` for async, `axum-test` for HTTP routes. 31 | - Locations: unit tests in-module (`mod tests`), integration tests in `tests/` (e.g., `tests/api_tests.rs`). 32 | - Conventions: name tests by behavior (e.g., `test_chat_completion_validation`). Ensure fast, deterministic tests. 33 | - Run: `cargo test` or `cargo test -p claude-code-api`. 34 | 35 | ## Commit & Pull Request Guidelines 36 | - Commits: concise, imperative subject (e.g., "Add models listing"), body explains motivation and approach; reference issues (`Closes #123`). 37 | - PRs: include summary, linked issues, test plan (commands/curl), config/env changes, and screenshots/logs if touching HTTP endpoints. 38 | - Add or update tests for user-visible changes. 39 | 40 | ## Security & Configuration Tips 41 | - Do not commit secrets. Use `.env` (see `.env.example`) and `config/local.toml` for local overrides. 42 | - Common env prefix: `CLAUDE_CODE__...` (see README). Validate setup with `script/check_config.sh`. 43 | - When changing defaults, document corresponding keys in `README.md` and `config/`. 44 | 45 | ## Agent-Specific Instructions 46 | - Keep patches minimal and scoped; avoid unrelated refactors or drive-by fixes. 47 | - Follow this AGENTS.md for style, structure, and command examples. 48 | - Reference files with explicit paths (e.g., `claude-code-api/src/main.rs:1`). 49 | - Prefer `rg` for search, `cargo` for build/test, and existing scripts in `script/`. 50 | 51 | ## Agent Tools & MCP 52 | - Enable tools in SDK: set `ClaudeCodeOptions.allowed_tools`/`disallowed_tools`, choose `permission_mode` (e.g., `AcceptEdits`). 53 | - Runtime approvals: implement `CanUseTool` and return `PermissionResult::{Allow,Deny}`; SDK会按控制协议回传 `{allow, input?/reason?}`。 54 | - MCP servers: configure via SDK `options.mcp_servers` (stdio/http/sse/sdk) or API `config/` + `script/start_with_mcp.sh`. 55 | - Programmatic agents: use builder `agents(...)` 与 `setting_sources(...)`,SDK 会透传 `--agents` 与 `--setting-sources`。 56 | - Streaming: `stream_input(...)` 发送后自动 `end_input()`;可启用 `include_partial_messages(true)` 获取部分块。 57 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/interactive.rs: -------------------------------------------------------------------------------- 1 | //! Interactive client example 2 | //! 3 | //! This example demonstrates how to use the SimpleInteractiveClient for 4 | //! interactive, stateful conversations with Claude. 5 | 6 | use cc_sdk::{ 7 | ClaudeCodeOptions, ContentBlock, Message, PermissionMode, Result, SimpleInteractiveClient, 8 | }; 9 | use std::io::{self, Write}; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | // Initialize logging 14 | tracing_subscriber::fmt() 15 | .with_env_filter("cc_sdk=debug,interactive=info") 16 | .init(); 17 | 18 | println!("Claude Code SDK - Interactive Client Example"); 19 | println!("Type 'quit' to exit\n"); 20 | 21 | // Create client with options 22 | let options = ClaudeCodeOptions::builder() 23 | .system_prompt("You are a helpful assistant.") 24 | .permission_mode(PermissionMode::AcceptEdits) 25 | .model("sonnet") 26 | .build(); 27 | 28 | let mut client = SimpleInteractiveClient::new(options)?; 29 | 30 | // Connect to Claude 31 | println!("Connecting to Claude..."); 32 | client.connect().await?; 33 | println!("Connected!\n"); 34 | 35 | println!("Ready for conversation. Type your message:"); 36 | 37 | // Interactive loop 38 | let stdin = io::stdin(); 39 | let mut input = String::new(); 40 | 41 | loop { 42 | print!("You: "); 43 | io::stdout().flush()?; 44 | 45 | input.clear(); 46 | stdin.read_line(&mut input)?; 47 | 48 | let input = input.trim(); 49 | 50 | if input.is_empty() { 51 | continue; 52 | } 53 | 54 | if input == "quit" { 55 | break; 56 | } 57 | 58 | // Send message and receive response 59 | let messages = client.send_and_receive(input.to_string()).await?; 60 | 61 | // Process response 62 | for msg in &messages { 63 | match msg { 64 | Message::Assistant { message } => { 65 | print!("Claude: "); 66 | for block in &message.content { 67 | match block { 68 | ContentBlock::Text(text) => { 69 | print!("{}", text.text); 70 | } 71 | ContentBlock::Thinking(thinking) => { 72 | println!("\n[Thinking: {}]", thinking.thinking); 73 | } 74 | ContentBlock::ToolUse(tool) => { 75 | println!("\n[Using tool: {} ({})]", tool.name, tool.id); 76 | } 77 | ContentBlock::ToolResult(result) => { 78 | println!("[Tool result for {}]", result.tool_use_id); 79 | } 80 | } 81 | } 82 | println!(); 83 | } 84 | Message::System { subtype, data: _ } => { 85 | if subtype != "thinking" { 86 | println!("[System: {subtype}]"); 87 | } 88 | } 89 | Message::Result { 90 | duration_ms, 91 | total_cost_usd, 92 | .. 93 | } => { 94 | print!("[Response time: {duration_ms}ms"); 95 | if let Some(cost) = total_cost_usd { 96 | print!(", cost: ${cost:.6}"); 97 | } 98 | println!("]"); 99 | } 100 | _ => {} 101 | } 102 | } 103 | println!(); 104 | } 105 | 106 | // Disconnect 107 | println!("\nDisconnecting..."); 108 | client.disconnect().await?; 109 | println!("Goodbye!"); 110 | 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/check_current_account.rs: -------------------------------------------------------------------------------- 1 | //! Example to check which Claude account is currently active 2 | //! 3 | //! This demonstrates that cc-sdk uses the same account as the Claude CLI 4 | //! that is currently logged in on your system. 5 | //! 6 | //! # How it works 7 | //! 8 | //! 1. The Claude CLI stores authentication in ~/.config/claude/ (or similar) 9 | //! 2. All terminal sessions share the same authentication 10 | //! 3. cc-sdk uses the same Claude CLI process, so it uses the same account 11 | //! 12 | //! # Usage 13 | //! 14 | //! ```bash 15 | //! # First, check which account Claude CLI is using 16 | //! echo "/status" | claude 17 | //! 18 | //! # Then run this example - it should use the same account 19 | //! cargo run --example check_current_account 20 | //! ``` 21 | 22 | use cc_sdk::{ClaudeCodeOptions, ClaudeSDKClient, Message, Result}; 23 | use futures::StreamExt; 24 | 25 | #[tokio::main] 26 | async fn main() -> Result<()> { 27 | println!("╔═══════════════════════════════════════════════╗"); 28 | println!("║ Current Claude Account Detection ║"); 29 | println!("╚═══════════════════════════════════════════════╝\n"); 30 | 31 | println!("This example demonstrates that cc-sdk uses the"); 32 | println!("same account as your current Claude CLI login.\n"); 33 | 34 | // Check if ANTHROPIC_USER_EMAIL is set 35 | if let Ok(email) = std::env::var("ANTHROPIC_USER_EMAIL") { 36 | println!("ℹ️ Environment variable set: {}", email); 37 | println!(" (This is used for SDK identification)\n"); 38 | } else { 39 | println!("ℹ️ ANTHROPIC_USER_EMAIL not set"); 40 | println!(" Will attempt to detect from CLI session\n"); 41 | } 42 | 43 | let options = ClaudeCodeOptions::builder() 44 | .max_turns(1) 45 | .build(); 46 | 47 | let mut client = ClaudeSDKClient::new(options); 48 | 49 | println!("📡 Connecting to Claude CLI..."); 50 | client.connect(None).await?; 51 | println!(" ✅ Connected\n"); 52 | 53 | // Method 1: Try get_account_info (uses env var if set) 54 | println!("🔍 Method 1: Using get_account_info()"); 55 | match client.get_account_info().await { 56 | Ok(info) => { 57 | println!(" ✅ Account detected: {}\n", info); 58 | } 59 | Err(e) => { 60 | println!(" ⚠️ Could not detect via env var: {}\n", e); 61 | } 62 | } 63 | 64 | // Method 2: Ask Claude who they are 65 | println!("🔍 Method 2: Asking Claude directly"); 66 | println!(" Sending query: 'What account am I using?'\n"); 67 | 68 | client.send_user_message( 69 | "What is the current account email or user that you're running under? \ 70 | Just tell me the account identifier, nothing else.".to_string() 71 | ).await?; 72 | 73 | let mut messages = client.receive_messages().await; 74 | while let Some(msg_result) = messages.next().await { 75 | match msg_result? { 76 | Message::Assistant { message } => { 77 | for block in message.content { 78 | if let cc_sdk::ContentBlock::Text(text) = block { 79 | println!(" 🤖 Response: {}\n", text.text); 80 | } 81 | } 82 | } 83 | Message::Result { .. } => break, 84 | _ => {} 85 | } 86 | } 87 | 88 | client.disconnect().await?; 89 | 90 | println!("─────────────────────────────────────────────────"); 91 | println!("\n📝 Summary:\n"); 92 | println!("• cc-sdk uses the Claude CLI that's installed on your system"); 93 | println!("• It inherits the authentication from Claude CLI"); 94 | println!("• All terminals/sessions use the SAME account"); 95 | println!("• To switch accounts, use: claude (and follow prompts)"); 96 | println!("• Or set ANTHROPIC_USER_EMAIL for SDK identification\n"); 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/demo_new_features.rs: -------------------------------------------------------------------------------- 1 | //! Demo of new v0.1.6 features without requiring Claude CLI 2 | //! 3 | //! This example demonstrates the new settings and add_dirs features 4 | 5 | use cc_sdk::ClaudeCodeOptions; 6 | use std::env; 7 | use std::path::PathBuf; 8 | 9 | fn main() { 10 | println!("Claude Code SDK v0.1.6 - New Features Demo"); 11 | println!("==========================================\n"); 12 | 13 | // Demo 1: Settings parameter 14 | println!("1. Settings Parameter:"); 15 | println!("----------------------"); 16 | 17 | let current_dir = env::current_dir().expect("Failed to get current directory"); 18 | let settings_path = current_dir.join("examples/claude-settings.json"); 19 | 20 | let options_with_settings = ClaudeCodeOptions::builder() 21 | .settings(settings_path.to_str().unwrap()) 22 | .model("claude-3-opus-20240229") 23 | .build(); 24 | 25 | println!( 26 | " Created options with settings: {:?}", 27 | options_with_settings.settings 28 | ); 29 | println!(); 30 | 31 | // Demo 2: Add single directory 32 | println!("2. Add Single Directory:"); 33 | println!("------------------------"); 34 | 35 | let options_single_dir = ClaudeCodeOptions::builder() 36 | .cwd("/main/project") 37 | .add_dir("/additional/project1") 38 | .add_dir("/additional/project2") 39 | .build(); 40 | 41 | println!(" Working directory: {:?}", options_single_dir.cwd); 42 | println!( 43 | " Additional directories: {:?}", 44 | options_single_dir.add_dirs 45 | ); 46 | println!(); 47 | 48 | // Demo 3: Add multiple directories at once 49 | println!("3. Add Multiple Directories:"); 50 | println!("----------------------------"); 51 | 52 | let dirs = vec![ 53 | PathBuf::from("/project/frontend"), 54 | PathBuf::from("/project/backend"), 55 | PathBuf::from("/project/shared"), 56 | ]; 57 | 58 | let options_multi_dir = ClaudeCodeOptions::builder().add_dirs(dirs.clone()).build(); 59 | 60 | println!( 61 | " Added {} directories at once:", 62 | options_multi_dir.add_dirs.len() 63 | ); 64 | for dir in &options_multi_dir.add_dirs { 65 | println!(" - {}", dir.display()); 66 | } 67 | println!(); 68 | 69 | // Demo 4: Complete configuration 70 | println!("4. Complete Configuration Example:"); 71 | println!("----------------------------------"); 72 | 73 | let complete_options = ClaudeCodeOptions::builder() 74 | // Working directory 75 | .cwd("/Users/zhangalex/Work/Projects/main") 76 | // Settings file 77 | .settings(settings_path.to_str().unwrap()) 78 | // Additional directories 79 | .add_dir("/Users/zhangalex/Work/Projects/lib1") 80 | .add_dir("/Users/zhangalex/Work/Projects/lib2") 81 | // Other options 82 | .system_prompt("You are an expert developer") 83 | .model("claude-3-opus-20240229") 84 | .permission_mode(cc_sdk::PermissionMode::AcceptEdits) 85 | .max_turns(10) 86 | .build(); 87 | 88 | println!(" Working directory: {:?}", complete_options.cwd); 89 | println!(" Settings file: {:?}", complete_options.settings); 90 | println!( 91 | " Additional dirs: {} directories", 92 | complete_options.add_dirs.len() 93 | ); 94 | println!(" Model: {:?}", complete_options.model); 95 | println!(" Permission mode: {:?}", complete_options.permission_mode); 96 | println!(" Max turns: {:?}", complete_options.max_turns); 97 | println!(); 98 | 99 | println!("✅ All new features are working correctly!"); 100 | println!("\nTo use these features with Claude CLI, run:"); 101 | println!(" cargo run --example test_settings"); 102 | println!(" cargo run --example test_add_dirs"); 103 | println!(" cargo run --example test_combined_features"); 104 | } 105 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/control_format_demo.rs: -------------------------------------------------------------------------------- 1 | //! Demonstration of control protocol format configuration 2 | //! 3 | //! This example shows how to configure the SDK to use different control protocol 4 | //! formats for compatibility with various CLI versions. 5 | 6 | use cc_sdk::{ClaudeCodeOptions, ControlProtocolFormat, ClaudeSDKClient, Result}; 7 | use futures::StreamExt; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | println!("=== Control Protocol Format Demo ===\n"); 12 | 13 | // Example 1: Use legacy format (default, maximum compatibility) 14 | println!("1. Using Legacy format (default):"); 15 | let options = ClaudeCodeOptions::builder() 16 | .system_prompt("You are a helpful assistant") 17 | .control_protocol_format(ControlProtocolFormat::Legacy) 18 | .build(); 19 | 20 | println!(" Format: Legacy (sdk_control_request/sdk_control_response)"); 21 | println!(" Compatible with: All CLI versions\n"); 22 | 23 | // Example 2: Use new format (for newer CLIs) 24 | println!("2. Using Control format:"); 25 | let _options_new = ClaudeCodeOptions::builder() 26 | .system_prompt("You are a helpful assistant") 27 | .control_protocol_format(ControlProtocolFormat::Control) 28 | .build(); 29 | 30 | println!(" Format: Control (type=control)"); 31 | println!(" Compatible with: Newer CLI versions only\n"); 32 | 33 | // Example 3: Auto-detect (future feature) 34 | println!("3. Using Auto format:"); 35 | let _options_auto = ClaudeCodeOptions::builder() 36 | .system_prompt("You are a helpful assistant") 37 | .control_protocol_format(ControlProtocolFormat::Auto) 38 | .build(); 39 | 40 | println!(" Format: Auto (defaults to Legacy for now)"); 41 | println!(" Future: Will detect CLI capabilities\n"); 42 | 43 | // Example 4: Environment variable override 44 | println!("4. Environment variable override:"); 45 | println!(" Set CLAUDE_CODE_CONTROL_FORMAT=legacy or control"); 46 | println!(" This overrides the programmatic setting\n"); 47 | 48 | // Demonstrate with actual client 49 | println!("5. Testing with actual client (Legacy format):"); 50 | let mut client = ClaudeSDKClient::new(options); 51 | 52 | match client.connect(Some("What is 2 + 2?".to_string())).await { 53 | Ok(_) => { 54 | println!(" ✓ Connected successfully"); 55 | 56 | // Receive response 57 | let mut messages = client.receive_messages().await; 58 | let mut response_count = 0; 59 | 60 | while let Some(msg) = messages.next().await { 61 | match msg { 62 | Ok(msg) => { 63 | response_count += 1; 64 | if response_count <= 3 { 65 | println!(" ✓ Received message: {msg:?}"); 66 | } 67 | 68 | // Stop after Result message 69 | if matches!(msg, cc_sdk::Message::Result { .. }) { 70 | break; 71 | } 72 | } 73 | Err(e) => { 74 | println!(" ✗ Error receiving message: {e}"); 75 | break; 76 | } 77 | } 78 | } 79 | 80 | client.disconnect().await?; 81 | println!(" ✓ Disconnected successfully"); 82 | } 83 | Err(e) => { 84 | println!(" ✗ Failed to connect: {e}"); 85 | println!(" Note: This might be expected if Claude CLI is not installed"); 86 | } 87 | } 88 | 89 | println!("\n=== Configuration Summary ==="); 90 | println!("• Default: Legacy format for maximum compatibility"); 91 | println!("• Can be changed via ClaudeCodeOptions::control_protocol_format"); 92 | println!("• Can be overridden via CLAUDE_CODE_CONTROL_FORMAT env var"); 93 | println!("• Receiving: Always supports both formats (dual-stack)"); 94 | println!("• Sending: Configurable based on your CLI version"); 95 | 96 | Ok(()) 97 | } -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/token_optimization_test.rs: -------------------------------------------------------------------------------- 1 | //! Tests for token optimization features 2 | 3 | use cc_sdk::token_tracker::{BudgetLimit, BudgetManager, BudgetStatus, TokenUsageTracker}; 4 | use cc_sdk::model_recommendation::ModelRecommendation; 5 | use cc_sdk::ClaudeCodeOptions; 6 | 7 | #[test] 8 | fn test_token_tracker_basic() { 9 | let mut tracker = TokenUsageTracker::new(); 10 | assert_eq!(tracker.total_tokens(), 0); 11 | 12 | tracker.update(100, 200, 0.05); 13 | assert_eq!(tracker.total_input_tokens, 100); 14 | assert_eq!(tracker.total_output_tokens, 200); 15 | assert_eq!(tracker.total_tokens(), 300); 16 | assert_eq!(tracker.total_cost_usd, 0.05); 17 | assert_eq!(tracker.session_count, 1); 18 | 19 | tracker.update(50, 100, 0.02); 20 | assert_eq!(tracker.total_tokens(), 450); 21 | assert_eq!(tracker.session_count, 2); 22 | } 23 | 24 | #[test] 25 | fn test_token_tracker_averages() { 26 | let mut tracker = TokenUsageTracker::new(); 27 | tracker.update(100, 100, 0.02); 28 | tracker.update(200, 200, 0.04); 29 | 30 | assert_eq!(tracker.avg_tokens_per_session(), 300.0); 31 | assert_eq!(tracker.avg_cost_per_session(), 0.03); 32 | } 33 | 34 | #[test] 35 | fn test_budget_limit_cost() { 36 | let limit = BudgetLimit::with_cost(1.0); 37 | 38 | let mut tracker = TokenUsageTracker::new(); 39 | tracker.update(100, 200, 0.5); 40 | assert!(matches!(limit.check_limits(&tracker), BudgetStatus::Ok)); 41 | 42 | tracker.update(100, 200, 0.35); 43 | assert!(matches!(limit.check_limits(&tracker), BudgetStatus::Warning { .. })); 44 | 45 | tracker.update(100, 200, 0.2); 46 | assert!(matches!(limit.check_limits(&tracker), BudgetStatus::Exceeded)); 47 | } 48 | 49 | #[test] 50 | fn test_budget_limit_tokens() { 51 | let limit = BudgetLimit::with_tokens(1000); 52 | 53 | let mut tracker = TokenUsageTracker::new(); 54 | tracker.update(300, 200, 0.05); 55 | assert!(matches!(limit.check_limits(&tracker), BudgetStatus::Ok)); 56 | 57 | tracker.update(300, 300, 0.05); 58 | assert!(matches!(limit.check_limits(&tracker), BudgetStatus::Exceeded)); 59 | } 60 | 61 | #[tokio::test] 62 | async fn test_budget_manager() { 63 | let manager = BudgetManager::new(); 64 | 65 | manager.set_limit(BudgetLimit::with_tokens(1000)).await; 66 | manager.update_usage(300, 200, 0.05).await; 67 | 68 | let usage = manager.get_usage().await; 69 | assert_eq!(usage.total_tokens(), 500); 70 | 71 | assert!(!manager.is_exceeded().await); 72 | 73 | manager.update_usage(300, 300, 0.05).await; 74 | assert!(manager.is_exceeded().await); 75 | } 76 | 77 | #[test] 78 | fn test_model_recommendations() { 79 | let recommender = ModelRecommendation::default(); 80 | 81 | assert_eq!(recommender.suggest("simple"), Some("claude-3-5-haiku-20241022")); 82 | assert_eq!(recommender.suggest("fast"), Some("claude-3-5-haiku-20241022")); 83 | // balanced now returns full Sonnet 4.5 model ID 84 | assert_eq!(recommender.suggest("balanced"), Some("claude-sonnet-4-5-20250929")); 85 | assert_eq!(recommender.suggest("complex"), Some("opus")); 86 | assert_eq!(recommender.suggest("unknown"), None); 87 | } 88 | 89 | #[test] 90 | fn test_custom_model_recommendations() { 91 | let mut recommender = ModelRecommendation::default(); 92 | 93 | recommender.add("my_task", "sonnet"); 94 | assert_eq!(recommender.suggest("my_task"), Some("sonnet")); 95 | 96 | recommender.remove("my_task"); 97 | assert_eq!(recommender.suggest("my_task"), None); 98 | } 99 | 100 | #[test] 101 | fn test_max_output_tokens_option() { 102 | let options = ClaudeCodeOptions::builder() 103 | .max_output_tokens(2000) 104 | .build(); 105 | 106 | assert_eq!(options.max_output_tokens, Some(2000)); 107 | } 108 | 109 | #[test] 110 | fn test_max_output_tokens_clamping() { 111 | // Should clamp to 32000 112 | let options = ClaudeCodeOptions::builder() 113 | .max_output_tokens(50000) 114 | .build(); 115 | 116 | assert_eq!(options.max_output_tokens, Some(32000)); 117 | 118 | // Should clamp to 1 119 | let options2 = ClaudeCodeOptions::builder() 120 | .max_output_tokens(0) 121 | .build(); 122 | 123 | assert_eq!(options2.max_output_tokens, Some(1)); 124 | } 125 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_auto_download.rs: -------------------------------------------------------------------------------- 1 | //! Test automatic CLI download functionality 2 | //! 3 | //! This example demonstrates and tests the auto-download feature. 4 | //! 5 | //! Run with: 6 | //! ```bash 7 | //! cargo run --example test_auto_download 8 | //! ``` 9 | 10 | use cc_sdk::ClaudeCodeOptions; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<(), Box> { 14 | // Initialize logging 15 | tracing_subscriber::fmt() 16 | .with_env_filter("cc_sdk=debug,info") 17 | .init(); 18 | 19 | println!("=== Claude Code CLI Auto-Download Test ===\n"); 20 | 21 | // Test 1: Check cache directory location 22 | println!("1. Cache Directory Information:"); 23 | if let Some(cache_dir) = cc_sdk::cli_download::get_cache_dir() { 24 | println!(" Cache directory: {}", cache_dir.display()); 25 | } else { 26 | println!(" ❌ Could not determine cache directory"); 27 | } 28 | 29 | if let Some(cli_path) = cc_sdk::cli_download::get_cached_cli_path() { 30 | println!(" Expected CLI path: {}", cli_path.display()); 31 | if cli_path.exists() { 32 | println!(" ✅ CLI is already cached!"); 33 | } else { 34 | println!(" ℹ️ CLI is not yet cached"); 35 | } 36 | } 37 | println!(); 38 | 39 | // Test 2: Try to find existing CLI 40 | println!("2. Searching for Existing CLI:"); 41 | match cc_sdk::transport::subprocess::find_claude_cli() { 42 | Ok(path) => { 43 | println!(" ✅ Found CLI at: {}", path.display()); 44 | println!(" No download needed!"); 45 | } 46 | Err(e) => { 47 | println!(" ℹ️ CLI not found in standard locations"); 48 | println!(" Details: {}", e); 49 | } 50 | } 51 | println!(); 52 | 53 | // Test 3: Test auto-download (optional - uncomment to actually download) 54 | println!("3. Auto-Download Test:"); 55 | println!(" To test auto-download, uncomment the code below and run again."); 56 | println!(); 57 | 58 | // Uncomment this block to test actual download: 59 | /* 60 | println!(" Attempting to download CLI..."); 61 | match cc_sdk::cli_download::download_cli(None, Some(Box::new(|downloaded, total| { 62 | if let Some(total) = total { 63 | println!(" Progress: {}/{} bytes", downloaded, total); 64 | } else { 65 | println!(" Downloaded: {} bytes", downloaded); 66 | } 67 | }))).await { 68 | Ok(path) => { 69 | println!(" ✅ CLI downloaded successfully to: {}", path.display()); 70 | } 71 | Err(e) => { 72 | println!(" ❌ Download failed: {}", e); 73 | } 74 | } 75 | */ 76 | println!(); 77 | 78 | // Test 4: Create options with auto_download_cli 79 | println!("4. ClaudeCodeOptions with auto_download_cli:"); 80 | let options = ClaudeCodeOptions::builder() 81 | .auto_download_cli(true) 82 | .model("sonnet") 83 | .build(); 84 | 85 | println!(" auto_download_cli: {}", options.auto_download_cli); 86 | println!(" model: {:?}", options.model); 87 | println!(); 88 | 89 | // Test 5: Demonstrate SubprocessTransport::new_async 90 | println!("5. Testing SubprocessTransport::new_async:"); 91 | println!(" This will attempt to find or download CLI..."); 92 | 93 | let options_for_transport = ClaudeCodeOptions::builder() 94 | .auto_download_cli(true) 95 | .build(); 96 | 97 | match cc_sdk::transport::SubprocessTransport::new_async(options_for_transport).await { 98 | Ok(transport) => { 99 | println!(" ✅ Transport created successfully!"); 100 | println!(" CLI is ready to use."); 101 | drop(transport); 102 | } 103 | Err(e) => { 104 | println!(" ❌ Failed to create transport: {}", e); 105 | println!(); 106 | println!(" This is expected if:"); 107 | println!(" - npm is not installed"); 108 | println!(" - Network is unavailable"); 109 | println!(" - Official install script is not accessible"); 110 | } 111 | } 112 | println!(); 113 | 114 | println!("=== Test Complete ==="); 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/docs/HOOK_EVENT_NAMES.md: -------------------------------------------------------------------------------- 1 | # Hook Event Names Reference 2 | 3 | ## ⚠️ Important: Event Names Must Use PascalCase 4 | 5 | When registering hooks with the Claude Code SDK, **event names must be in PascalCase** to match the CLI's expectations. Using snake_case or other formats will cause hooks to never trigger. 6 | 7 | ## ✅ Correct Event Names 8 | 9 | | Hook Event | Correct Format | ❌ Wrong Format | 10 | |------------|----------------|-----------------| 11 | | Pre Tool Use | `"PreToolUse"` | `"pre_tool_use"`, `"pre-tool-use"` | 12 | | Post Tool Use | `"PostToolUse"` | `"post_tool_use"`, `"post-tool-use"` | 13 | | User Prompt Submit | `"UserPromptSubmit"` | `"user_prompt_submit"`, `"user-prompt-submit"` | 14 | | Stop | `"Stop"` | `"stop"` | 15 | | Subagent Stop | `"SubagentStop"` | `"subagent_stop"`, `"subagent-stop"` | 16 | | Pre Compact | `"PreCompact"` | `"pre_compact"`, `"pre-compact"` | 17 | 18 | ## 📝 Example Usage 19 | 20 | ### Correct ✅ 21 | 22 | ```rust 23 | use cc_sdk::{ClaudeCodeOptions, HookMatcher}; 24 | use std::collections::HashMap; 25 | 26 | let mut hooks = HashMap::new(); 27 | 28 | // ✅ Correct: PascalCase 29 | hooks.insert( 30 | "PreToolUse".to_string(), 31 | vec![HookMatcher { 32 | matcher: Some(serde_json::json!("*")), 33 | hooks: vec![Arc::new(MyHook)], 34 | }], 35 | ); 36 | 37 | hooks.insert( 38 | "PostToolUse".to_string(), 39 | vec![HookMatcher { 40 | matcher: Some(serde_json::json!("*")), 41 | hooks: vec![Arc::new(MyHook)], 42 | }], 43 | ); 44 | ``` 45 | 46 | ### Incorrect ❌ 47 | 48 | ```rust 49 | // ❌ Wrong: snake_case - hooks will never trigger! 50 | hooks.insert( 51 | "pre_tool_use".to_string(), // This will NOT work 52 | vec![...], 53 | ); 54 | 55 | // ❌ Wrong: kebab-case - hooks will never trigger! 56 | hooks.insert( 57 | "post-tool-use".to_string(), // This will NOT work 58 | vec![...], 59 | ); 60 | ``` 61 | 62 | ## 🔍 Why PascalCase? 63 | 64 | The Claude Code CLI uses PascalCase for all event names in its internal protocol. When you register a hook: 65 | 66 | 1. **SDK → CLI**: The SDK sends hook configurations to the CLI with the event names you provide 67 | 2. **CLI Processing**: The CLI matches incoming events against registered hook names 68 | 3. **Event Triggering**: Only exact matches (case-sensitive) will trigger your hooks 69 | 70 | **Example Flow**: 71 | 72 | ``` 73 | 1. You register: "PreToolUse" 74 | ✅ CLI receives: "PreToolUse" 75 | ✅ CLI event fires: "PreToolUse" 76 | ✅ Match! → Hook executes 77 | 78 | 2. You register: "pre_tool_use" 79 | ❌ CLI receives: "pre_tool_use" 80 | ❌ CLI event fires: "PreToolUse" 81 | ❌ No match → Hook never executes 82 | ``` 83 | 84 | ## 🧪 Debugging Hook Registration 85 | 86 | If your hooks aren't triggering, check: 87 | 88 | 1. **Event Name Format**: Ensure PascalCase 89 | ```rust 90 | // Check your registration 91 | println!("Registered hooks: {:?}", hooks.keys()); 92 | // Should see: ["PreToolUse", "PostToolUse"] 93 | // NOT: ["pre_tool_use", "post_tool_use"] 94 | ``` 95 | 96 | 2. **Enable Debug Output**: 97 | ```rust 98 | options.debug_stderr = Some(Arc::new(Mutex::new(std::io::stderr()))); 99 | ``` 100 | 101 | 3. **Check Hook Initialization**: 102 | Look for CLI logs showing registered hooks: 103 | ``` 104 | Hooks registered: { 105 | "PreToolUse": [...], 106 | "PostToolUse": [...] 107 | } 108 | ``` 109 | 110 | ## 📚 See Also 111 | 112 | - [Hook Examples](../examples/hooks_typed.rs) - Complete working example with strongly-typed hooks 113 | - [Control Protocol Demo](../examples/control_protocol_demo.rs) - Advanced usage with permissions and hooks 114 | - [Implementation Summary](../../HOOK_TYPES_IMPLEMENTATION_SUMMARY.md) - Technical details of the hook system 115 | 116 | ## 🔗 Related to Type Safety 117 | 118 | The strongly-typed `HookInput` enum also uses PascalCase in its variant names, which aligns with the event names: 119 | 120 | ```rust 121 | pub enum HookInput { 122 | #[serde(rename = "PreToolUse")] // ← Same as event name 123 | PreToolUse(PreToolUseHookInput), 124 | 125 | #[serde(rename = "PostToolUse")] // ← Same as event name 126 | PostToolUse(PostToolUseHookInput), 127 | 128 | // ... 129 | } 130 | ``` 131 | 132 | This consistency helps ensure correctness across the entire hook system. 133 | -------------------------------------------------------------------------------- /claude-code-api/src/models/claude.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Clone)] 7 | #[serde(tag = "type", rename_all = "snake_case")] 8 | pub enum ClaudeStreamEvent { 9 | MessageStart { 10 | message: ClaudeMessage, 11 | }, 12 | ContentBlockStart { 13 | index: i32, 14 | content_block: ContentBlock, 15 | }, 16 | ContentBlockDelta { 17 | index: i32, 18 | delta: ContentDelta, 19 | }, 20 | ContentBlockStop { 21 | index: i32, 22 | }, 23 | MessageDelta { 24 | delta: MessageDelta, 25 | usage: Usage, 26 | }, 27 | MessageStop, 28 | Error { 29 | error: ClaudeError, 30 | }, 31 | } 32 | 33 | #[derive(Debug, Serialize, Deserialize, Clone)] 34 | pub struct ClaudeMessage { 35 | pub id: String, 36 | pub r#type: String, 37 | pub role: String, 38 | pub content: Vec, 39 | pub model: String, 40 | pub stop_reason: Option, 41 | pub stop_sequence: Option, 42 | pub usage: Usage, 43 | } 44 | 45 | #[derive(Debug, Serialize, Deserialize, Clone)] 46 | #[serde(tag = "type")] 47 | pub enum ContentBlock { 48 | #[serde(rename = "text")] 49 | Text { text: String }, 50 | } 51 | 52 | #[derive(Debug, Serialize, Deserialize, Clone)] 53 | #[serde(tag = "type")] 54 | pub enum ContentDelta { 55 | #[serde(rename = "text_delta")] 56 | TextDelta { text: String }, 57 | } 58 | 59 | #[derive(Debug, Serialize, Deserialize, Clone)] 60 | pub struct MessageDelta { 61 | pub stop_reason: Option, 62 | pub stop_sequence: Option, 63 | } 64 | 65 | #[derive(Debug, Serialize, Deserialize, Clone)] 66 | pub struct Usage { 67 | pub input_tokens: i32, 68 | pub output_tokens: i32, 69 | } 70 | 71 | #[derive(Debug, Serialize, Deserialize, Clone)] 72 | pub struct ClaudeError { 73 | pub r#type: String, 74 | pub message: String, 75 | } 76 | 77 | #[derive(Debug, Serialize, Deserialize, Clone)] 78 | pub struct ClaudeCodeOutput { 79 | pub r#type: String, 80 | #[serde(skip_serializing_if = "Option::is_none")] 81 | pub subtype: Option, 82 | #[serde(flatten)] 83 | pub data: Value, 84 | } 85 | 86 | #[derive(Debug, Clone)] 87 | pub struct ClaudeModel { 88 | pub id: String, 89 | pub display_name: String, 90 | pub context_window: i32, 91 | } 92 | 93 | impl ClaudeModel { 94 | pub fn all() -> Vec { 95 | vec![ 96 | // Claude 4 Series (2025) 97 | Self { 98 | id: "claude-opus-4-1-20250805".to_string(), 99 | display_name: "Claude Opus 4.1".to_string(), 100 | context_window: 500000, 101 | }, 102 | Self { 103 | id: "claude-opus-4-20250514".to_string(), 104 | display_name: "Claude Opus 4".to_string(), 105 | context_window: 500000, 106 | }, 107 | Self { 108 | id: "claude-sonnet-4-20250514".to_string(), 109 | display_name: "Claude Sonnet 4".to_string(), 110 | context_window: 500000, 111 | }, 112 | // Claude 3.7 Series (2025) 113 | Self { 114 | id: "claude-3-7-sonnet-20250219".to_string(), 115 | display_name: "Claude Sonnet 3.7".to_string(), 116 | context_window: 200000, 117 | }, 118 | Self { 119 | id: "claude-3-7-sonnet-latest".to_string(), 120 | display_name: "Claude Sonnet 3.7 (Latest)".to_string(), 121 | context_window: 200000, 122 | }, 123 | // Claude 3.5 Series (2024) 124 | Self { 125 | id: "claude-3-5-haiku-20241022".to_string(), 126 | display_name: "Claude Haiku 3.5".to_string(), 127 | context_window: 200000, 128 | }, 129 | Self { 130 | id: "claude-3-5-haiku-latest".to_string(), 131 | display_name: "Claude Haiku 3.5 (Latest)".to_string(), 132 | context_window: 200000, 133 | }, 134 | // Claude 3 Series (2024) 135 | Self { 136 | id: "claude-3-haiku-20240307".to_string(), 137 | display_name: "Claude Haiku 3".to_string(), 138 | context_window: 200000, 139 | }, 140 | ] 141 | } 142 | } -------------------------------------------------------------------------------- /docs/url-preview-troubleshooting.md: -------------------------------------------------------------------------------- 1 | # url-preview Troubleshooting Guide 2 | 3 | ## Common Issues and Solutions 4 | 5 | ### 1. "Invalid model name" Error 6 | 7 | **Problem**: 8 | ``` 9 | Error: API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"system: Invalid model name"}} 10 | ``` 11 | 12 | **Cause**: Using `claude-3-opus-20240229` which is not a valid model name for Claude CLI. 13 | 14 | **Solution**: Use one of these valid model names: 15 | - `claude-3-5-haiku-20241022` 16 | - `claude-3-5-sonnet-20241022` 17 | - `opus` (alias for latest Opus) 18 | - `sonnet` (alias for latest Sonnet) 19 | 20 | **Fix in url-preview**: 21 | ```rust 22 | // Change from: 23 | "claude-3-opus-20240229" 24 | // To: 25 | "opus" // or any other valid model 26 | ``` 27 | 28 | ### 2. "No function call or valid JSON in response" Error 29 | 30 | **Problem**: 31 | ``` 32 | Error: External service error: OpenAI - No function call or valid JSON in response 33 | ``` 34 | 35 | **Cause**: The response doesn't contain `tool_calls` array that url-preview expects. 36 | 37 | **Possible Reasons**: 38 | 1. Claude didn't return JSON in its response 39 | 2. The JSON detection failed 40 | 3. Old version of claude-code-api that only supports function_call 41 | 42 | **Solution**: 43 | 1. Make sure you're using the latest claude-code-api with tool_calls support 44 | 2. Check claude-code-api logs to see what Claude is returning 45 | 3. Try a simpler prompt to test JSON extraction 46 | 47 | ### 3. Connection Refused 48 | 49 | **Problem**: 50 | ``` 51 | Error: Connection refused (os error 61) 52 | ``` 53 | 54 | **Solution**: Start claude-code-api: 55 | ```bash 56 | cargo run --bin ccapi 57 | # or 58 | ./target/release/ccapi 59 | ``` 60 | 61 | ### 4. Timeout Errors 62 | 63 | **Problem**: Request times out after 30-60 seconds 64 | 65 | **Solution**: 66 | 1. Set longer timeout in `.env`: 67 | ``` 68 | CLAUDE_CODE__CLAUDE__TIMEOUT_SECONDS=600 69 | ``` 70 | 2. Use a faster model (haiku) 71 | 3. Reduce content size or simplify schema 72 | 73 | ## Debugging Steps 74 | 75 | ### 1. Test claude-code-api Directly 76 | 77 | ```bash 78 | # Test if API is running 79 | curl http://localhost:8080/health 80 | 81 | # Test tool calling 82 | curl -X POST http://localhost:8080/v1/chat/completions \ 83 | -H "Content-Type: application/json" \ 84 | -d '{ 85 | "model": "claude-3-5-haiku-20241022", 86 | "messages": [{"role": "user", "content": "Return JSON: {\"test\": \"hello\"}"}], 87 | "tools": [{ 88 | "type": "function", 89 | "function": { 90 | "name": "test", 91 | "description": "Test function", 92 | "parameters": {"type": "object", "properties": {"test": {"type": "string"}}} 93 | } 94 | }] 95 | }' | jq '.' 96 | ``` 97 | 98 | ### 2. Check Response Format 99 | 100 | The response should have this structure: 101 | ```json 102 | { 103 | "choices": [{ 104 | "message": { 105 | "tool_calls": [{ 106 | "id": "call_xxx", 107 | "type": "function", 108 | "function": { 109 | "name": "extract_data", 110 | "arguments": "{\"title\": \"...\", ...}" 111 | } 112 | }] 113 | } 114 | }] 115 | } 116 | ``` 117 | 118 | ### 3. Enable Debug Logging 119 | 120 | ```bash 121 | # Run claude-code-api with debug logging 122 | RUST_LOG=debug cargo run --bin ccapi 123 | 124 | # Run url-preview with debug logging 125 | RUST_LOG=url_preview=debug cargo run --example claude_api_working --features llm 126 | ``` 127 | 128 | ### 4. Check url-preview Code 129 | 130 | url-preview checks for responses in this order: 131 | 1. `message.tool_calls` (preferred) 132 | 2. `message.content` with JSON extraction (fallback) 133 | 134 | Make sure your version of url-preview has this fallback logic. 135 | 136 | ## Working Configuration 137 | 138 | **claude-code-api `.env`**: 139 | ```env 140 | CLAUDE_CODE__SERVER__PORT=8080 141 | CLAUDE_CODE__CLAUDE__COMMAND=claude 142 | CLAUDE_CODE__CLAUDE__TIMEOUT_SECONDS=600 143 | RUST_LOG=claude_code_api=info 144 | ``` 145 | 146 | **url-preview code**: 147 | ```rust 148 | let config = OpenAIConfig::new() 149 | .with_api_base("http://localhost:8080/v1") 150 | .with_api_key("not-needed"); 151 | 152 | let provider = OpenAIProvider::from_config( 153 | config, 154 | "claude-3-5-haiku-20241022".to_string() // Valid model! 155 | ); 156 | ``` 157 | 158 | ## Testing Script 159 | 160 | Save this as `test_url_preview.sh`: 161 | ```bash 162 | #!/bin/bash 163 | ./test_url_preview_integration.sh 164 | ``` 165 | 166 | This will test the complete integration and show you exactly what's happening. -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/permission_modes.rs: -------------------------------------------------------------------------------- 1 | //! Permission modes example 2 | //! 3 | //! This example demonstrates different permission modes for file operations 4 | 5 | use cc_sdk::{ClaudeCodeOptions, Message, PermissionMode, Result, query}; 6 | use futures::StreamExt; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | // Initialize logging 11 | tracing_subscriber::fmt() 12 | .with_env_filter("cc_sdk=info") 13 | .init(); 14 | 15 | println!("Claude Code SDK - Permission Modes Example\n"); 16 | 17 | // Example 1: Default mode (prompts for permission) 18 | println!("Example 1: Default permission mode"); 19 | println!("----------------------------------"); 20 | println!("This mode will prompt for permission before writing files."); 21 | println!("Since we're using --print mode, the prompt won't be shown.\n"); 22 | 23 | let options = ClaudeCodeOptions::builder() 24 | .system_prompt("You are a helpful coding assistant.") 25 | .permission_mode(PermissionMode::Default) 26 | .build(); 27 | 28 | run_query("Create a file called test1.txt with 'Hello World'", options).await?; 29 | 30 | println!("\n"); 31 | 32 | // Example 2: AcceptEdits mode (auto-accepts edit prompts) 33 | println!("Example 2: AcceptEdits permission mode"); 34 | println!("--------------------------------------"); 35 | println!("This mode automatically accepts edit prompts but still checks permissions.\n"); 36 | 37 | let options = ClaudeCodeOptions::builder() 38 | .system_prompt("You are a helpful coding assistant.") 39 | .permission_mode(PermissionMode::AcceptEdits) 40 | .allowed_tools(vec!["write".to_string(), "edit".to_string()]) 41 | .build(); 42 | 43 | run_query("Try to create a file called test2.txt", options).await?; 44 | 45 | println!("\n"); 46 | 47 | // Example 3: BypassPermissions mode (allows all operations) 48 | println!("Example 3: BypassPermissions mode"); 49 | println!("---------------------------------"); 50 | println!("This mode allows all tool operations without prompting."); 51 | println!("USE WITH CAUTION - only in trusted environments!\n"); 52 | 53 | let options = ClaudeCodeOptions::builder() 54 | .system_prompt("You are a helpful coding assistant.") 55 | .permission_mode(PermissionMode::BypassPermissions) 56 | .build(); 57 | 58 | run_query("List files in current directory", options).await?; 59 | 60 | println!("\n"); 61 | 62 | // Example 4: Restricted tools with AcceptEdits 63 | println!("Example 4: Restricted tools with AcceptEdits"); 64 | println!("--------------------------------------------"); 65 | println!("Only allows specific tools, auto-accepts those operations.\n"); 66 | 67 | let options = ClaudeCodeOptions::builder() 68 | .system_prompt("You are a helpful coding assistant.") 69 | .permission_mode(PermissionMode::AcceptEdits) 70 | .allowed_tools(vec!["read".to_string()]) 71 | .disallowed_tools(vec!["write".to_string(), "bash".to_string()]) 72 | .build(); 73 | 74 | run_query("Try to read and write a file", options).await?; 75 | 76 | Ok(()) 77 | } 78 | 79 | async fn run_query(prompt: &str, options: ClaudeCodeOptions) -> Result<()> { 80 | println!("Query: {prompt}"); 81 | println!("Permission mode: {:?}", options.permission_mode); 82 | 83 | let mut messages = query(prompt, Some(options)).await?; 84 | 85 | while let Some(msg) = messages.next().await { 86 | match msg? { 87 | Message::Assistant { message } => { 88 | for block in &message.content { 89 | match block { 90 | cc_sdk::ContentBlock::Text(text) => { 91 | println!("Claude: {}", text.text); 92 | } 93 | cc_sdk::ContentBlock::ToolUse(tool_use) => { 94 | println!( 95 | "Claude wants to use tool: {} ({})", 96 | tool_use.name, tool_use.id 97 | ); 98 | } 99 | _ => {} 100 | } 101 | } 102 | } 103 | Message::Result { 104 | duration_ms, 105 | is_error, 106 | .. 107 | } => { 108 | if is_error { 109 | println!("Query completed with error in {duration_ms}ms"); 110 | } else { 111 | println!("Query completed successfully in {duration_ms}ms"); 112 | } 113 | break; 114 | } 115 | _ => {} 116 | } 117 | } 118 | 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /claude-code-api/src/utils/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{ 2 | claude::{ClaudeCodeOutput, ClaudeStreamEvent, ContentDelta}, 3 | openai::{ChatCompletionStreamResponse, StreamChoice, DeltaMessage}, 4 | }; 5 | use chrono::Utc; 6 | use uuid::Uuid; 7 | 8 | pub fn claude_to_openai_stream( 9 | claude_output: ClaudeCodeOutput, 10 | model: &str, 11 | ) -> Option { 12 | match claude_output.r#type.as_str() { 13 | "assistant" => { 14 | // 处理助手消息 15 | if let Some(message) = claude_output.data.get("message") 16 | && let Some(content_array) = message.get("content").and_then(|c| c.as_array()) { 17 | for content in content_array { 18 | if let Some(text) = content.get("text").and_then(|t| t.as_str()) { 19 | return Some(ChatCompletionStreamResponse { 20 | id: Uuid::new_v4().to_string(), 21 | object: "chat.completion.chunk".to_string(), 22 | created: Utc::now().timestamp(), 23 | model: model.to_string(), 24 | choices: vec![StreamChoice { 25 | index: 0, 26 | delta: DeltaMessage { 27 | role: Some("assistant".to_string()), 28 | content: Some(text.to_string()), 29 | }, 30 | finish_reason: None, 31 | }], 32 | }); 33 | } 34 | } 35 | } 36 | } 37 | "result" => { 38 | // 会话结束 39 | return Some(ChatCompletionStreamResponse { 40 | id: Uuid::new_v4().to_string(), 41 | object: "chat.completion.chunk".to_string(), 42 | created: Utc::now().timestamp(), 43 | model: model.to_string(), 44 | choices: vec![StreamChoice { 45 | index: 0, 46 | delta: DeltaMessage::default(), 47 | finish_reason: Some("stop".to_string()), 48 | }], 49 | }); 50 | } 51 | _ => {} 52 | } 53 | 54 | None 55 | } 56 | 57 | #[allow(dead_code)] 58 | fn convert_claude_event_to_openai( 59 | event: ClaudeStreamEvent, 60 | model: &str, 61 | ) -> Option { 62 | match event { 63 | ClaudeStreamEvent::MessageStart { .. } => { 64 | Some(ChatCompletionStreamResponse { 65 | id: Uuid::new_v4().to_string(), 66 | object: "chat.completion.chunk".to_string(), 67 | created: Utc::now().timestamp(), 68 | model: model.to_string(), 69 | choices: vec![StreamChoice { 70 | index: 0, 71 | delta: DeltaMessage { 72 | role: Some("assistant".to_string()), 73 | content: None, 74 | }, 75 | finish_reason: None, 76 | }], 77 | }) 78 | } 79 | ClaudeStreamEvent::ContentBlockDelta { delta, .. } => { 80 | match delta { 81 | ContentDelta::TextDelta { text } => { 82 | Some(ChatCompletionStreamResponse { 83 | id: Uuid::new_v4().to_string(), 84 | object: "chat.completion.chunk".to_string(), 85 | created: Utc::now().timestamp(), 86 | model: model.to_string(), 87 | choices: vec![StreamChoice { 88 | index: 0, 89 | delta: DeltaMessage { 90 | role: None, 91 | content: Some(text), 92 | }, 93 | finish_reason: None, 94 | }], 95 | }) 96 | } 97 | } 98 | } 99 | ClaudeStreamEvent::MessageStop => { 100 | Some(ChatCompletionStreamResponse { 101 | id: Uuid::new_v4().to_string(), 102 | object: "chat.completion.chunk".to_string(), 103 | created: Utc::now().timestamp(), 104 | model: model.to_string(), 105 | choices: vec![StreamChoice { 106 | index: 0, 107 | delta: DeltaMessage::default(), 108 | finish_reason: Some("stop".to_string()), 109 | }], 110 | }) 111 | } 112 | _ => None, 113 | } 114 | } -------------------------------------------------------------------------------- /doc/INTERACTIVE_SESSION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Interactive Session Guide 2 | 3 | ## Overview 4 | 5 | The Interactive Session feature allows the Claude Code API to reuse the same Claude process for multiple requests within the same conversation. This significantly improves performance by avoiding the overhead of creating new processes for each request. 6 | 7 | ## Benefits 8 | 9 | 1. **Faster Response Times**: After the initial request, subsequent requests in the same conversation are much faster (typically 1-3 seconds vs 5-15 seconds) 10 | 2. **Context Preservation**: The Claude process maintains context between requests, enabling true multi-turn conversations 11 | 3. **Resource Efficiency**: Reduces system resource usage by reusing processes instead of creating new ones 12 | 13 | ## Configuration 14 | 15 | Interactive sessions are enabled by default. You can control this feature through configuration: 16 | 17 | ### In Configuration Files 18 | 19 | ```toml 20 | [claude] 21 | # Enable interactive session management 22 | use_interactive_sessions = true 23 | ``` 24 | 25 | ### Via Environment Variables 26 | 27 | ```bash 28 | export CLAUDE_CODE__CLAUDE__USE_INTERACTIVE_SESSIONS=true 29 | ``` 30 | 31 | ## How It Works 32 | 33 | 1. When a request includes a `conversation_id`, the system checks if an interactive session already exists for that ID 34 | 2. If a session exists, the message is sent to the existing Claude process 35 | 3. If no session exists, a new interactive Claude process is started and associated with the conversation ID 36 | 4. Sessions are automatically cleaned up after 30 minutes of inactivity 37 | 38 | ## API Usage 39 | 40 | Simply include a `conversation_id` in your requests to use the same session: 41 | 42 | ```bash 43 | # First request - creates a new session 44 | curl -X POST http://localhost:8000/v1/chat/completions \ 45 | -H "Content-Type: application/json" \ 46 | -d '{ 47 | "model": "claude-3-5-sonnet-20241022", 48 | "conversation_id": "my-session-123", 49 | "messages": [{ 50 | "role": "user", 51 | "content": "Hello! Remember the number 42." 52 | }] 53 | }' 54 | 55 | # Second request - reuses the same session 56 | curl -X POST http://localhost:8000/v1/chat/completions \ 57 | -H "Content-Type: application/json" \ 58 | -d '{ 59 | "model": "claude-3-5-sonnet-20241022", 60 | "conversation_id": "my-session-123", 61 | "messages": [{ 62 | "role": "user", 63 | "content": "What number did I ask you to remember?" 64 | }] 65 | }' 66 | ``` 67 | 68 | ## Session Management 69 | 70 | ### Session Lifetime 71 | 72 | - Sessions are kept alive for 30 minutes after the last activity 73 | - Inactive sessions are automatically cleaned up to free resources 74 | - You can manually close a session by making a DELETE request to `/v1/sessions/{conversation_id}` 75 | 76 | ### Session Limits 77 | 78 | - Maximum concurrent sessions is controlled by `claude.max_concurrent_sessions` configuration 79 | - Default: 20 concurrent sessions 80 | 81 | ### Monitoring 82 | 83 | You can check active sessions through the stats endpoint: 84 | 85 | ```bash 86 | curl http://localhost:8000/stats 87 | ``` 88 | 89 | ## Performance Comparison 90 | 91 | | Request Type | Without Interactive Sessions | With Interactive Sessions | 92 | |--------------|------------------------------|---------------------------| 93 | | First Request | 5-15 seconds | 5-15 seconds | 94 | | Subsequent Requests | 5-15 seconds | 1-3 seconds | 95 | 96 | ## Best Practices 97 | 98 | 1. **Use Consistent Conversation IDs**: Always use the same conversation ID for related requests 99 | 2. **Generate Unique IDs**: Use UUIDs or similar to ensure conversation IDs don't collide 100 | 3. **Handle Session Expiry**: Be prepared to handle cases where a session has expired and needs recreation 101 | 4. **Close Long Sessions**: For very long conversations, consider closing and recreating sessions periodically 102 | 103 | ## Troubleshooting 104 | 105 | ### Session Not Reusing 106 | 107 | If sessions aren't being reused: 108 | 1. Check that `use_interactive_sessions` is set to `true` 109 | 2. Verify you're using the same `conversation_id` across requests 110 | 3. Ensure the session hasn't expired (30 minute timeout) 111 | 4. Check logs for any errors creating or maintaining sessions 112 | 113 | ### High Memory Usage 114 | 115 | If memory usage is high: 116 | 1. Reduce `max_concurrent_sessions` 117 | 2. Decrease session timeout in configuration 118 | 3. Manually close sessions when conversations end 119 | 120 | ## Testing 121 | 122 | Use the provided test script to verify interactive sessions are working: 123 | 124 | ```bash 125 | ./script/test_interactive_session.sh 126 | ``` 127 | 128 | This script will: 129 | - Create a session and ask Claude to remember information 130 | - Make a second request to verify the session was reused 131 | - Check that different conversation IDs create separate sessions -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/code_generator.rs: -------------------------------------------------------------------------------- 1 | //! Code Generator Example using Claude Code SDK 2 | //! 3 | //! This example shows how to use the SDK to generate Rust code solutions 4 | //! with tests and documentation. 5 | 6 | use cc_sdk::{ClaudeCodeOptions, InteractiveClient, PermissionMode, Result}; 7 | use std::time::Instant; 8 | 9 | async fn generate_rust_solution(question: &str, project_name: &str) -> Result<()> { 10 | println!("🚀 Generating Rust solution for: {question}"); 11 | println!("📁 Project name: {project_name}"); 12 | println!("{}", "=".repeat(60)); 13 | 14 | // Configure Claude for code generation 15 | let options = ClaudeCodeOptions::builder() 16 | .system_prompt( 17 | "You are an expert Rust developer. Create clean, idiomatic Rust code \ 18 | with comprehensive tests and documentation.", 19 | ) 20 | .model("sonnet") 21 | .permission_mode(PermissionMode::AcceptEdits) 22 | .allowed_tools(vec![ 23 | "bash".to_string(), 24 | "write_file".to_string(), 25 | "edit_file".to_string(), 26 | "read_file".to_string(), 27 | ]) 28 | .max_turns(20) 29 | .build(); 30 | 31 | let mut client = InteractiveClient::new(options)?; 32 | let start_time = Instant::now(); 33 | 34 | // Connect to Claude 35 | client.connect().await?; 36 | println!("✅ Connected to Claude\n"); 37 | 38 | // Step 1: Generate the solution 39 | println!("📝 Step 1: Generating Rust code..."); 40 | let prompt = format!( 41 | "Create a new Rust project called '{project_name}' that solves this problem: {question}. \ 42 | Include comprehensive unit tests and proper error handling." 43 | ); 44 | 45 | let messages = client.send_and_receive(prompt).await?; 46 | print_claude_response(&messages); 47 | 48 | // Step 2: Verify the solution 49 | println!("\n🔍 Step 2: Verifying the solution..."); 50 | let verify_prompt = format!( 51 | "Please run 'cargo check', 'cargo test', and 'cargo clippy' on the {project_name} project \ 52 | to ensure everything is correct. Fix any issues found." 53 | ); 54 | 55 | let messages = client.send_and_receive(verify_prompt).await?; 56 | print_claude_response(&messages); 57 | 58 | // Step 3: Add documentation 59 | println!("\n📚 Step 3: Adding documentation..."); 60 | let doc_prompt = format!( 61 | "Add a comprehensive README.md to the {project_name} project explaining the solution, \ 62 | how to use it, and include examples." 63 | ); 64 | 65 | let messages = client.send_and_receive(doc_prompt).await?; 66 | print_claude_response(&messages); 67 | 68 | // Disconnect 69 | client.disconnect().await?; 70 | 71 | let duration = start_time.elapsed(); 72 | println!("\n✨ Solution generated successfully!"); 73 | println!("⏱️ Total time: {:.2} seconds", duration.as_secs_f64()); 74 | println!("{}", "=".repeat(60)); 75 | 76 | Ok(()) 77 | } 78 | 79 | fn print_claude_response(messages: &[cc_sdk::Message]) { 80 | for msg in messages { 81 | if let cc_sdk::Message::Assistant { message } = msg { 82 | for content in &message.content { 83 | if let cc_sdk::ContentBlock::Text(text) = content { 84 | // Only print first 500 chars to keep output readable 85 | let preview = if text.text.len() > 500 { 86 | format!("{}...", &text.text[..500]) 87 | } else { 88 | text.text.clone() 89 | }; 90 | println!("{preview}"); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | #[tokio::main] 98 | async fn main() -> Result<()> { 99 | println!("🦀 Claude Code SDK - Rust Code Generator Example\n"); 100 | 101 | // Example problems to solve 102 | let examples = [("Binary Search Implementation", "binary_search"), 103 | ("LRU Cache with Generics", "lru_cache"), 104 | ("Thread-Safe Counter", "safe_counter")]; 105 | 106 | // Process each example 107 | for (i, (question, project_name)) in examples.iter().enumerate() { 108 | println!("\n📌 Example {}: {}\n", i + 1, question); 109 | 110 | match generate_rust_solution(question, project_name).await { 111 | Ok(_) => println!("✅ Successfully generated: {project_name}"), 112 | Err(e) => eprintln!("❌ Failed to generate {project_name}: {e:?}"), 113 | } 114 | 115 | // Add delay between examples to avoid rate limits 116 | if i < examples.len() - 1 { 117 | println!("\n⏳ Waiting 5 seconds before next example..."); 118 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 119 | } 120 | } 121 | 122 | println!("\n🎉 All examples completed!"); 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /docs/tool-calling.md: -------------------------------------------------------------------------------- 1 | # OpenAI Tool Calling Support 2 | 3 | This document describes the OpenAI tool calling support in claude-code-api. 4 | 5 | ## Overview 6 | 7 | Claude Code API supports OpenAI's modern `tools` format for AI tool integrations. The deprecated `functions` format is no longer supported. 8 | 9 | ## How It Works 10 | 11 | When a request includes `tools` parameter, the API will: 12 | 13 | 1. Pass the user's message to Claude Code 14 | 2. Analyze Claude's response for JSON content 15 | 3. If JSON is detected that matches a requested tool, format it as a tool_calls response 16 | 4. Return the response in OpenAI's expected format 17 | 18 | ## Request Format 19 | 20 | ```json 21 | { 22 | "model": "claude-3-5-haiku-20241022", 23 | "messages": [ 24 | { 25 | "role": "user", 26 | "content": "Please preview this URL: https://example.com" 27 | } 28 | ], 29 | "tools": [ 30 | { 31 | "type": "function", 32 | "function": { 33 | "name": "url_preview", 34 | "description": "Preview a URL and extract its content", 35 | "parameters": { 36 | "type": "object", 37 | "properties": { 38 | "url": { 39 | "type": "string", 40 | "description": "The URL to preview" 41 | } 42 | }, 43 | "required": ["url"] 44 | } 45 | } 46 | } 47 | ], 48 | "tool_choice": "auto" 49 | } 50 | ``` 51 | 52 | ## Response Format 53 | 54 | When a tool call is detected, the response will include: 55 | 56 | ```json 57 | { 58 | "id": "chatcmpl-...", 59 | "object": "chat.completion", 60 | "created": 1234567890, 61 | "model": "claude-3-5-haiku-20241022", 62 | "choices": [ 63 | { 64 | "index": 0, 65 | "message": { 66 | "role": "assistant", 67 | "content": null, 68 | "tool_calls": [ 69 | { 70 | "id": "call_xxx", 71 | "type": "function", 72 | "function": { 73 | "name": "url_preview", 74 | "arguments": "{\"url\": \"https://example.com\"}" 75 | } 76 | } 77 | ] 78 | }, 79 | "finish_reason": "stop" 80 | } 81 | ], 82 | "usage": { 83 | "prompt_tokens": 0, 84 | "completion_tokens": 50, 85 | "total_tokens": 50 86 | } 87 | } 88 | ``` 89 | 90 | ## Integration Examples 91 | 92 | ### url-preview Integration 93 | 94 | The tool calling support was designed to work seamlessly with libraries like url-preview: 95 | 96 | ```rust 97 | use url_preview::{LLMExtractor, OpenAIProvider}; 98 | use async_openai::config::OpenAIConfig; 99 | 100 | // Configure for claude-code-api 101 | let config = OpenAIConfig::new() 102 | .with_api_base("http://localhost:8080/v1") 103 | .with_api_key("not-needed"); 104 | 105 | let provider = OpenAIProvider::from_config( 106 | config, 107 | "claude-3-5-haiku-20241022".to_string() 108 | ); 109 | ``` 110 | 111 | ### Python Example 112 | 113 | ```python 114 | from openai import OpenAI 115 | 116 | client = OpenAI( 117 | base_url="http://localhost:8080/v1", 118 | api_key="not-needed" 119 | ) 120 | 121 | response = client.chat.completions.create( 122 | model="claude-3-5-haiku-20241022", 123 | messages=[{"role": "user", "content": "Get weather in Beijing"}], 124 | tools=[{ 125 | "type": "function", 126 | "function": { 127 | "name": "get_weather", 128 | "description": "Get weather for a location", 129 | "parameters": { 130 | "type": "object", 131 | "properties": { 132 | "location": {"type": "string"} 133 | }, 134 | "required": ["location"] 135 | } 136 | } 137 | }] 138 | ) 139 | 140 | # Access tool calls 141 | if response.choices[0].message.tool_calls: 142 | tool_call = response.choices[0].message.tool_calls[0] 143 | print(f"Tool: {tool_call.function.name}") 144 | print(f"Args: {tool_call.function.arguments}") 145 | ``` 146 | 147 | ## JSON Detection 148 | 149 | The API uses multiple strategies to detect JSON in Claude's responses: 150 | 151 | 1. Direct JSON parsing of the entire response 152 | 2. JSON within markdown code blocks (```json) 153 | 3. Embedded JSON objects within text 154 | 155 | ## Valid Model Names 156 | 157 | Use these model names with claude-code-api: 158 | - `claude-3-5-haiku-20241022` (fastest) 159 | - `claude-3-5-sonnet-20241022` (balanced) 160 | - `opus` (most capable, alias) 161 | - `sonnet` (alias) 162 | 163 | ## Limitations 164 | 165 | - Tool calling is only supported in non-streaming mode 166 | - The API does not execute tools; it only formats responses as tool calls 167 | - Complex nested tool parameters may require additional validation 168 | 169 | ## Testing 170 | 171 | Use the provided test script to verify tool calling: 172 | 173 | ```bash 174 | ./test_tool_format.sh 175 | ``` -------------------------------------------------------------------------------- /claude-code-sdk-rs/examples/test_combined_features.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating combined use of new features 2 | //! 3 | //! This example shows how to use both settings and add_dirs together 4 | 5 | use cc_sdk::{ClaudeCodeOptions, InteractiveClient, Result}; 6 | use std::env; 7 | use std::path::PathBuf; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | // Initialize logging 12 | tracing_subscriber::fmt() 13 | .with_env_filter("cc_sdk=info") 14 | .init(); 15 | 16 | println!("Testing combined features: settings + add_dirs\n"); 17 | println!("===================================================\n"); 18 | 19 | // Get the current directory to build absolute paths 20 | let current_dir = env::current_dir().expect("Failed to get current directory"); 21 | 22 | // Build absolute path for settings file 23 | let settings_path = current_dir.join("examples/custom-claude-settings.json"); 24 | let settings_str = settings_path.to_str().expect("Invalid path"); 25 | 26 | // Check if settings file exists 27 | if !settings_path.exists() { 28 | println!("Warning: Settings file not found at: {settings_str}"); 29 | println!("Creating example will continue without settings file.\n"); 30 | } 31 | 32 | // Create directories list 33 | let project_dirs = vec![ 34 | PathBuf::from("/Users/zhangalex/Work/Projects/FW/rust-claude-code-api"), 35 | PathBuf::from("/Users/zhangalex/Work/Projects/FW/claude-code-sdk-python"), 36 | ]; 37 | 38 | // Build options with all new features 39 | let mut builder = ClaudeCodeOptions::builder() 40 | // Set primary working directory 41 | .cwd("/Users/zhangalex/Work/Projects/FW/rust-claude-code-api") 42 | // Add additional directories 43 | .add_dirs(project_dirs.clone()) 44 | // Add one more directory individually 45 | .add_dir("/Users/zhangalex/Work/Projects/FW/url-preview"); 46 | 47 | // Only add settings if file exists 48 | if settings_path.exists() { 49 | builder = builder.settings(settings_str); 50 | } 51 | 52 | let options = builder 53 | // Set other options 54 | .system_prompt( 55 | "You are an expert Rust and Python developer with access to multiple projects", 56 | ) 57 | .model("claude-3-opus-20240229") 58 | .permission_mode(cc_sdk::PermissionMode::AcceptEdits) 59 | .max_turns(10) 60 | .build(); 61 | 62 | println!("Configuration:"); 63 | println!("--------------"); 64 | println!("Settings file: {:?}", options.settings); 65 | println!("Working directory: {:?}", options.cwd); 66 | println!("Additional directories: {:?}", options.add_dirs); 67 | println!("Model: {:?}", options.model); 68 | println!("Permission mode: {:?}", options.permission_mode); 69 | println!(); 70 | 71 | // Use interactive client for a more realistic test 72 | let mut client = InteractiveClient::new(options)?; 73 | 74 | // Connect to Claude 75 | println!("Connecting to Claude Code..."); 76 | client.connect().await?; 77 | 78 | // Send a message that might utilize multiple directories 79 | println!("Sending query...\n"); 80 | client 81 | .send_message( 82 | "Can you analyze the structure of these projects and tell me: 83 | 1. What are the main differences between the Rust and Python SDK implementations? 84 | 2. Are there any features in one that are missing in the other? 85 | Please be concise." 86 | .to_string(), 87 | ) 88 | .await?; 89 | 90 | // Receive the response 91 | println!("Claude's response:\n"); 92 | println!("==================\n"); 93 | 94 | let messages = client.receive_response().await?; 95 | 96 | for msg in messages { 97 | match msg { 98 | cc_sdk::Message::Assistant { message } => { 99 | for block in message.content { 100 | match block { 101 | cc_sdk::ContentBlock::Text(text) => { 102 | println!("{}", text.text); 103 | } 104 | cc_sdk::ContentBlock::ToolUse(tool) => { 105 | println!("[Using tool: {}]", tool.name); 106 | } 107 | _ => {} 108 | } 109 | } 110 | } 111 | cc_sdk::Message::Result { 112 | duration_ms, 113 | total_cost_usd, 114 | .. 115 | } => { 116 | println!("\n---"); 117 | println!("Session completed"); 118 | println!("Duration: {duration_ms}ms"); 119 | if let Some(cost) = total_cost_usd { 120 | println!("Cost: ${cost:.6}"); 121 | } 122 | } 123 | _ => {} 124 | } 125 | } 126 | 127 | // Disconnect 128 | client.disconnect().await?; 129 | println!("\nDisconnected successfully!"); 130 | 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /claude-code-sdk-rs/tests/control_protocol_e2e.rs: -------------------------------------------------------------------------------- 1 | use cc_sdk::Query; 2 | use cc_sdk::transport::Transport; 3 | use cc_sdk::{ 4 | CanUseTool, HookMatcher, PermissionResult, PermissionResultAllow, 5 | ToolPermissionContext, Message, 6 | }; 7 | use cc_sdk::Result; 8 | use async_trait::async_trait; 9 | use futures::stream::{self, Stream}; 10 | use serde_json::json; 11 | use std::pin::Pin; 12 | use std::sync::Arc; 13 | use tokio::sync::{mpsc, Mutex}; 14 | 15 | struct MockTransport { 16 | inbound_ctrl_rx: Option>, 17 | sent_ctrl_responses: Arc>>, 18 | } 19 | 20 | impl MockTransport { 21 | fn new(rx: mpsc::Receiver) -> Self { 22 | Self { 23 | inbound_ctrl_rx: Some(rx), 24 | sent_ctrl_responses: Arc::new(Mutex::new(Vec::new())), 25 | } 26 | } 27 | } 28 | 29 | #[async_trait] 30 | impl Transport for MockTransport { 31 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { 32 | self 33 | } 34 | 35 | async fn connect(&mut self) -> Result<()> { 36 | Ok(()) 37 | } 38 | 39 | async fn send_message(&mut self, _message: cc_sdk::transport::InputMessage) -> Result<()> { 40 | Ok(()) 41 | } 42 | 43 | fn receive_messages(&mut self) -> Pin> + Send + 'static>> { 44 | Box::pin(stream::empty()) 45 | } 46 | 47 | async fn send_control_request(&mut self, _request: cc_sdk::ControlRequest) -> Result<()> { 48 | Ok(()) 49 | } 50 | 51 | async fn receive_control_response( 52 | &mut self, 53 | ) -> Result> { 54 | Ok(None) 55 | } 56 | 57 | async fn send_sdk_control_request(&mut self, _request: serde_json::Value) -> Result<()> { 58 | Ok(()) 59 | } 60 | 61 | async fn send_sdk_control_response(&mut self, response: serde_json::Value) -> Result<()> { 62 | // Mimic SubprocessTransport wrapping 63 | let wrapped = serde_json::json!({ 64 | "type": "control_response", 65 | "response": response 66 | }); 67 | self.sent_ctrl_responses.lock().await.push(wrapped); 68 | Ok(()) 69 | } 70 | 71 | fn is_connected(&self) -> bool { 72 | true 73 | } 74 | 75 | async fn disconnect(&mut self) -> Result<()> { 76 | Ok(()) 77 | } 78 | 79 | fn take_sdk_control_receiver( 80 | &mut self, 81 | ) -> Option> { 82 | self.inbound_ctrl_rx.take() 83 | } 84 | } 85 | 86 | struct AllowAll; 87 | 88 | #[async_trait] 89 | impl CanUseTool for AllowAll { 90 | async fn can_use_tool( 91 | &self, 92 | _tool_name: &str, 93 | _input: &serde_json::Value, 94 | _context: &ToolPermissionContext, 95 | ) -> PermissionResult { 96 | PermissionResult::Allow(PermissionResultAllow { 97 | updated_input: Some(json!({"safe": true})), 98 | updated_permissions: None, 99 | }) 100 | } 101 | } 102 | 103 | #[tokio::test] 104 | async fn e2e_can_use_tool_allow() -> Result<()> { 105 | // Prepare inbound control request from CLI → SDK 106 | let (tx, rx) = mpsc::channel(10); 107 | let request = json!({ 108 | "type": "control_request", 109 | "request_id": "req_123", 110 | "request": { 111 | "subtype": "can_use_tool", 112 | "tool_name": "Write", 113 | "input": {"path":"/tmp/demo.txt"}, 114 | "permission_suggestions": [] 115 | } 116 | }); 117 | tx.send(request).await.unwrap(); 118 | 119 | // Build transport and query 120 | let mock = MockTransport::new(rx); 121 | let sent_responses = mock.sent_ctrl_responses.clone(); 122 | let transport: Arc>> = 123 | Arc::new(Mutex::new(Box::new(mock))); 124 | 125 | let can_use = Some(Arc::new(AllowAll) as Arc); 126 | let hooks: Option>> = None; 127 | let sdk_mcp_servers = std::collections::HashMap::new(); 128 | 129 | let mut query = Query::new( 130 | transport.clone(), 131 | true, 132 | can_use, 133 | hooks, 134 | sdk_mcp_servers, 135 | ); 136 | 137 | // Start and allow background control handling 138 | query.start().await?; 139 | 140 | // Wait briefly for processing 141 | tokio::time::sleep(std::time::Duration::from_millis(50)).await; 142 | 143 | // Verify a control_response was sent with expected shape 144 | let responses = sent_responses.lock().await; 145 | assert!(!responses.is_empty(), "No control response sent"); 146 | let outer = responses.last().unwrap(); 147 | assert_eq!(outer["type"], "control_response"); 148 | let resp = &outer["response"]; 149 | assert_eq!(resp["subtype"], "success"); 150 | assert_eq!(resp["request_id"], "req_123"); 151 | assert_eq!(resp["response"]["allow"], true); 152 | assert_eq!(resp["response"]["input"]["safe"], true); 153 | 154 | Ok(()) 155 | } 156 | --------------------------------------------------------------------------------