volition-core 0.2.0

Core library for the Volition AI agent
Documentation
# How to Add an API Provider

This guide explains how to add a new API provider to the Volition core system. We'll use Anthropic as an example, but the same pattern applies to other API providers.

## 1. Create the Provider File

Create a new file in `volition-core/src/providers/` named after your provider (e.g., `anthropic.rs`).

## 2. Define the Provider Structure

```rust
use super::Provider;
use crate::config::ModelConfig;
use crate::models::chat::{ApiResponse, ChatMessage, Choice};
use crate::models::tools::{ToolCall, ToolDefinition};
use anyhow::{Result, anyhow, Context};
use async_trait::async_trait;
use reqwest::Client;
use serde_json::{json, Value};
use tracing::debug;

pub struct AnthropicProvider {
    config: ModelConfig,
    http_client: Client,
    api_key: String,
}

impl AnthropicProvider {
    pub fn new(config: ModelConfig, http_client: Client, api_key: String) -> Self {
        debug!("Creating new Anthropic provider with model: {}", config.model_name);
        Self {
            config,
            http_client,
            api_key,
        }
    }
}
```

## 3. Implement the Provider Trait

Implement the `Provider` trait for your provider:

```rust
#[async_trait]
impl Provider for AnthropicProvider {
    fn name(&self) -> &str {
        &self.config.model_name
    }

    async fn get_completion(
        &self,
        messages: Vec<ChatMessage>,
        tools: Option<&[ToolDefinition]>,
    ) -> Result<ApiResponse> {
        self.call_chat_completion_api(messages, tools).await
    }
}
```

## 4. Implement API Methods

Add the necessary methods for API communication:

```rust
impl AnthropicProvider {
    fn build_payload(
        &self,
        messages: Vec<ChatMessage>,
        tools: Option<&[ToolDefinition]>,
    ) -> Result<Value> {
        debug!("Building Anthropic payload...");
        debug!("Model name: {}", self.config.model_name);
        debug!("Message count: {}", messages.len());

        let mut payload = json!({
            "model": self.config.model_name,
            "messages": messages.iter().map(|msg| {
                json!({
                    "role": msg.role,
                    "content": msg.content.as_deref().unwrap_or_default()
                })
            }).collect::<Vec<_>>()
        });

        // Add tools if present
        if let Some(tools) = tools {
            if !tools.is_empty() {
                let tools_with_type: Vec<Value> = tools
                    .iter()
                    .map(|t| {
                        json!({
                            "type": "function",
                            "function": {
                                "name": t.name,
                                "description": t.description,
                                "parameters": t.parameters
                            }
                        })
                    })
                    .collect();
                payload["tools"] = json!(tools_with_type);
            }
        }

        // Add parameters if present
        if let Some(params) = &self.config.parameters {
            if let Some(temperature) = params.get("temperature").and_then(|t| t.as_float()) {
                payload["temperature"] = json!(temperature);
            }
        }

        debug!("Final payload: {}", serde_json::to_string_pretty(&payload)?);
        Ok(payload)
    }

    fn parse_response(&self, response_body: &str) -> Result<ApiResponse> {
        debug!("Parsing Anthropic response...");
        debug!("Response body: {}", response_body);

        let raw_response: Value = serde_json::from_str(response_body)?;
        
        // Parse the response into ApiResponse format
        // This will be specific to the Anthropic API response structure
        todo!("Implement response parsing")
    }

    async fn call_chat_completion_api(
        &self,
        messages: Vec<ChatMessage>,
        tools: Option<&[ToolDefinition]>,
    ) -> Result<ApiResponse> {
        let endpoint = self.config.endpoint.as_deref().unwrap_or("https://api.anthropic.com/v1/messages");
        debug!("Using Anthropic endpoint: {}", endpoint);

        let payload = self.build_payload(messages, tools)?;

        debug!("Sending request to Anthropic API...");
        let response = self
            .http_client
            .post(endpoint)
            .header("Content-Type", "application/json")
            .header("x-api-key", &self.api_key)
            .json(&payload)
            .send()
            .await
            .context("Failed to send request to Anthropic API")?;

        debug!("Received response from Anthropic API, status: {}", response.status());
        let response_body = response
            .text()
            .await
            .context("Failed to read response from Anthropic API")?;

        self.parse_response(&response_body)
    }
}
```

## Key Considerations

1. **Error Handling**: Implement proper error handling for API requests and responses.
2. **Rate Limiting**: Consider implementing rate limiting if the API has restrictions.
3. **Authentication**: Handle API authentication appropriately using the provider's `api_key` field.
4. **Response Parsing**: Ensure proper parsing of API responses into the `ApiResponse` format.
5. **Tool Support**: If the API supports tools/functions, implement the necessary mapping.
6. **Testing**: Add tests for your provider implementation.
7. **Configuration**: Support both default and custom endpoints through the provider's constructor.

## Example Implementation

For a complete example, see the implementation of other providers in the codebase:

- `volition-core/src/providers/openai.rs`
- `volition-core/src/providers/gemini.rs`
- `volition-core/src/providers/ollama.rs`

## Testing Your Provider

1. Create unit tests for your provider implementation
2. Test error cases and edge conditions
3. Test tool/function calling if supported
4. Test rate limiting and retry logic if implemented
5. Test both default and custom endpoint configurations

## Adding to the Build System

If your provider requires additional dependencies, add them to `volition-core/Cargo.toml`:

```toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde_json = "1.0"
# Add other dependencies as needed
```