Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
# ClawForge environment configuration (example)
# Copy to `.env` and adjust. `.env` is git-ignored.

# ── Runtime (existing ClawForge agent runtime) ───────────────────────────────
# LLM provider key used by the planner crate.
# ── Runtime: model providers ─────────────────────────────────────────────────
# Set the API key for any provider you want; the runtime registers each one
# whose variable is present. See docs/model-providers.md for the full list.

# Aggregator (one key, many models). Optional override: OLLAMA_URL for local.
OPENROUTER_API_KEY="sk-or-v1-..."
OLLAMA_URL="http://localhost:11434"

# Global / Western providers
# OPENAI_API_KEY=""
# ANTHROPIC_API_KEY=""
# GEMINI_API_KEY=""
# MISTRAL_API_KEY=""
# XAI_API_KEY=""
# GROQ_API_KEY=""
# TOGETHER_API_KEY=""
# FIREWORKS_API_KEY=""
# PERPLEXITY_API_KEY=""
# COHERE_API_KEY=""

# Chinese providers
# DEEPSEEK_API_KEY="" # DeepSeek
# DASHSCOPE_API_KEY="" # Alibaba Qwen (DashScope)
# ZHIPU_API_KEY="" # Zhipu AI (GLM)
# MOONSHOT_API_KEY="" # Moonshot AI (Kimi)
# QIANFAN_API_KEY="" # Baidu ERNIE (Qianfan)
# MINIMAX_API_KEY="" # MiniMax
# HUNYUAN_API_KEY="" # Tencent Hunyuan
# YI_API_KEY="" # 01.AI Yi
# STEPFUN_API_KEY="" # StepFun
# BAICHUAN_API_KEY="" # Baichuan AI
# SPARK_API_KEY="" # iFlytek Spark
# SENSENOVA_API_KEY="" # SenseTime SenseNova

# Bind the gateway to Tailscale for secure remote access (1 to enable).
CLAWFORGE_ENABLE_TAILSCALE=0

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,25 @@ compliance report — in memory. See [docs/demo.md](docs/demo.md).
### Running the full runtime (optional)

```bash
export OPENROUTER_API_KEY="sk-or-v1-..."
# Set a key for any provider(s) you want; the runtime registers each one present.
export ANTHROPIC_API_KEY="sk-ant-..." # or OPENAI_API_KEY, DEEPSEEK_API_KEY, OPENROUTER_API_KEY, ...
cargo run -p clawforge-cli -- serve --port 3000 # local-first gateway

cd frontend && npm install && npm run dev # dashboard (separate terminal)
# or: docker-compose up --build
```

ClawForge is multi-provider: OpenAI, Anthropic, Google Gemini, Mistral, xAI,
Groq, OpenRouter, local Ollama, and the major Chinese providers (DeepSeek, Qwen,
Zhipu GLM, Moonshot/Kimi, Baidu ERNIE, MiniMax, Tencent Hunyuan, 01.AI Yi,
StepFun, Baichuan, iFlytek, SenseTime). See [docs/model-providers.md](docs/model-providers.md).

Configuration is environment-driven; see [.env.example](.env.example) and
[docs/installation.md](docs/installation.md).

## Documentation

- **Start here:** [Product positioning](docs/product-positioning.md) · [Architecture](docs/architecture.md) · [Diagrams](docs/diagrams.md) · [Installation](docs/installation.md) · [Demo](docs/demo.md)
- **Start here:** [Product positioning](docs/product-positioning.md) · [Architecture](docs/architecture.md) · [Diagrams](docs/diagrams.md) · [Installation](docs/installation.md) · [Model providers](docs/model-providers.md) · [Demo](docs/demo.md)
- **Use cases:** [Overview](docs/use-cases.md) · [Government municipality](docs/government-municipality.md) · [Enterprise IT](docs/enterprise.md)
- **Operations:** [UAE PDPL note](docs/uae-pdpl.md) · [Security disclaimer](docs/security-disclaimer.md) · [Roadmap](docs/roadmap.md) · [Limitations](docs/limitations.md)
- **Contributing:** [Developer guide](docs/developer-guide.md) · [CONTRIBUTING.md](CONTRIBUTING.md)
Expand Down
2 changes: 1 addition & 1 deletion backend/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ uuid = { workspace = true }
chrono = { workspace = true }
clap = { version = "4", features = ["derive"] }
axum = { workspace = true, features = ["ws"] }
tower-http = { version = "0.6", features = ["cors", "trace"] }
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
tokio-stream = { version = "0.1", features = ["sync"] }
futures = "0.3"
reqwest = { version = "0.12", features = ["json"] }
Expand Down
9 changes: 9 additions & 0 deletions backend/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ async fn run_server(config: Config) -> Result<()> {
info!(url = %url, "Registered Ollama provider");
}

// Register every other catalog provider whose API key is present in the
// environment (OpenAI, Anthropic, Google, Mistral, xAI, Groq, and the major
// Chinese providers: DeepSeek, Qwen, Zhipu/GLM, Moonshot/Kimi, Baidu ERNIE,
// MiniMax, Tencent Hunyuan, 01.AI Yi, StepFun, Baichuan, iFlytek, SenseTime).
let extra = clawforge_planner::providers::catalog::register_from_env(&mut registry);
if !extra.is_empty() {
info!(providers = ?extra, "Registered additional model providers from environment");
}

let registry = Arc::new(registry);

// Wire up components
Expand Down
2 changes: 1 addition & 1 deletion backend/executor/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tracing::{debug, error, info, warn};
use uuid::Uuid;

use clawforge_core::{
AuditEventPayload, ClawError, Component, Event, EventKind,
AuditEventPayload, Capabilities, ClawError, Component, Event, EventKind,
Message, ProposedAction,
tools::ToolRegistry,
};
Expand Down
153 changes: 153 additions & 0 deletions backend/planner/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use std::time::Instant;

use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::debug;

use clawforge_core::{LlmProvider, LlmRequest, LlmResponse};

/// Native Anthropic provider using the Messages API.
///
/// Anthropic does not use the OpenAI chat-completions wire format, so it gets a
/// dedicated client: `POST {base}/v1/messages` with the `x-api-key` and
/// `anthropic-version` headers, a top-level `system` field, and a `messages`
/// array.
pub struct AnthropicProvider {
client: Client,
api_key: String,
base_url: String,
version: String,
}

impl AnthropicProvider {
/// Build a provider from an API key (uses the public API base URL).
pub fn new(api_key: impl Into<String>) -> Self {
Self {
client: Client::new(),
api_key: api_key.into(),
base_url: "https://api.anthropic.com".to_string(),
version: "2023-06-01".to_string(),
}
}

/// Override the base URL (e.g. a gateway or Bedrock-style proxy).
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
}

#[derive(Serialize)]
struct MessagesRequest {
model: String,
max_tokens: u32,
temperature: f32,
#[serde(skip_serializing_if = "String::is_empty")]
system: String,
messages: Vec<Message>,
}

#[derive(Serialize)]
struct Message {
role: String,
content: String,
}

#[derive(Deserialize)]
struct MessagesResponse {
content: Vec<ContentBlock>,
usage: Option<AnthropicUsage>,
}

#[derive(Deserialize)]
struct ContentBlock {
#[serde(default)]
text: String,
}

#[derive(Deserialize)]
struct AnthropicUsage {
#[serde(default)]
input_tokens: u64,
#[serde(default)]
output_tokens: u64,
}

#[async_trait]
impl LlmProvider for AnthropicProvider {
fn name(&self) -> &str {
"anthropic"
}

async fn complete(&self, request: &LlmRequest) -> Result<LlmResponse> {
let start = Instant::now();

let body = MessagesRequest {
model: request.model.clone(),
max_tokens: request.max_tokens,
temperature: request.temperature,
system: request.system_prompt.clone(),
messages: vec![Message {
role: "user".to_string(),
content: request.user_prompt.clone(),
}],
};

debug!(model = %request.model, "Sending request to Anthropic");

let response = self
.client
.post(format!("{}/v1/messages", self.base_url.trim_end_matches('/')))
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.version)
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.context("Anthropic HTTP request failed")?;

let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
anyhow::bail!("Anthropic returned {}: {}", status, error_body);
}

let parsed: MessagesResponse = response
.json()
.await
.context("Failed to parse Anthropic response")?;

let content = parsed
.content
.iter()
.map(|b| b.text.as_str())
.collect::<Vec<_>>()
.join("");

let tokens_used = parsed
.usage
.map(|u| u.input_tokens + u.output_tokens)
.unwrap_or(0);
let latency_ms = start.elapsed().as_millis() as u64;

Ok(LlmResponse {
content,
provider: "anthropic".to_string(),
model: request.model.clone(),
tokens_used,
latency_ms,
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn name_is_anthropic() {
assert_eq!(AnthropicProvider::new("sk-test").name(), "anthropic");
}
}
Loading
Loading