Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ For production workloads, use the [WhatsApp Cloud API](https://developers.facebo

3 native drivers (Anthropic, Gemini, OpenAI-compatible) route to 27 providers:

Anthropic, Gemini, OpenAI, Groq, DeepSeek, OpenRouter, Together, Mistral, Fireworks, Cohere, Perplexity, xAI, AI21, Cerebras, SambaNova, HuggingFace, Replicate, Ollama, vLLM, LM Studio, Qwen, MiniMax, Zhipu, Moonshot, Qianfan, Bedrock, and more.
Anthropic, Gemini, OpenAI, Groq, Volcengine, DeepSeek, OpenRouter, Together, Mistral, Fireworks, Cohere, Perplexity, xAI, AI21, Cerebras, SambaNova, HuggingFace, Replicate, Ollama, vLLM, LM Studio, Qwen, MiniMax, Zhipu, Moonshot, Qianfan, Bedrock, and more.

Intelligent routing with task complexity scoring, automatic fallback, cost tracking, and per-model pricing.

Expand Down
2 changes: 1 addition & 1 deletion crates/openfang-channels/src/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl LineAdapter {
diff |= a ^ b;
}
if diff != 0 {
let computed = base64::engine::general_purpose::STANDARD.encode(&result);
let computed = base64::engine::general_purpose::STANDARD.encode(result);
// Log first/last 4 chars of each signature for debugging without leaking full HMAC
let comp_redacted = format!(
"{}...{}",
Expand Down
1 change: 1 addition & 0 deletions crates/openfang-cli/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[
("ANTHROPIC_API_KEY", "Anthropic"),
("OPENAI_API_KEY", "OpenAI"),
("DEEPSEEK_API_KEY", "DeepSeek"),
("VOLCENGINE_API_KEY", "Volcano Engine"),
("GEMINI_API_KEY", "Gemini"),
("GOOGLE_API_KEY", "Gemini"),
("GROQ_API_KEY", "Groq"),
Expand Down
22 changes: 22 additions & 0 deletions crates/openfang-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,18 @@ fn provider_list() -> Vec<(&'static str, &'static str, &'static str, &'static st
("groq", "GROQ_API_KEY", "llama-3.3-70b-versatile", "Groq"),
("gemini", "GEMINI_API_KEY", "gemini-2.5-flash", "Gemini"),
("deepseek", "DEEPSEEK_API_KEY", "deepseek-chat", "DeepSeek"),
(
"volcengine_coding",
"VOLCENGINE_API_KEY",
"ark-code-latest",
"Volcano Engine Coding Plan",
),
(
"volcengine",
"VOLCENGINE_API_KEY",
"doubao-seed-1-6-251015",
"Volcano Engine",
),
(
"anthropic",
"ANTHROPIC_API_KEY",
Expand Down Expand Up @@ -4541,6 +4553,7 @@ fn provider_to_env_var(provider: &str) -> String {
"perplexity" => "PERPLEXITY_API_KEY".to_string(),
"cohere" => "COHERE_API_KEY".to_string(),
"xai" => "XAI_API_KEY".to_string(),
"volcengine" | "doubao" | "volcengine_coding" => "VOLCENGINE_API_KEY".to_string(),
"brave" => "BRAVE_API_KEY".to_string(),
"tavily" => "TAVILY_API_KEY".to_string(),
other => format!("{}_API_KEY", other.to_uppercase()),
Expand Down Expand Up @@ -4592,6 +4605,15 @@ pub(crate) fn test_api_key(provider: &str, env_var: &str) -> bool {
.get("https://openrouter.ai/api/v1/models")
.bearer_auth(&key)
.send(),
"volcengine" | "doubao" => {
let base = openfang_types::model_catalog::VOLCENGINE_BASE_URL.trim_end_matches('/');
client.get(format!("{base}/models")).bearer_auth(&key).send()
}
"volcengine_coding" => {
let base = openfang_types::model_catalog::VOLCENGINE_CODING_BASE_URL
.trim_end_matches('/');
client.get(format!("{base}/models")).bearer_auth(&key).send()
}
_ => return true, // unknown provider — skip test
};

Expand Down
16 changes: 16 additions & 0 deletions crates/openfang-cli/src/tui/screens/init_wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ const PROVIDERS: &[ProviderInfo] = &[
needs_key: true,
hint: "",
},
ProviderInfo {
name: "volcengine_coding",
display: "Volcano Engine Coding Plan",
env_var: "VOLCENGINE_API_KEY",
default_model: "ark-code-latest",
needs_key: true,
hint: "",
},
ProviderInfo {
name: "volcengine",
display: "Volcano Engine",
env_var: "VOLCENGINE_API_KEY",
default_model: "doubao-seed-1-6-251015",
needs_key: true,
hint: "",
},
ProviderInfo {
name: "openrouter",
display: "OpenRouter",
Expand Down
1 change: 1 addition & 0 deletions crates/openfang-cli/src/tui/screens/welcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[
("ANTHROPIC_API_KEY", "Anthropic"),
("OPENAI_API_KEY", "OpenAI"),
("DEEPSEEK_API_KEY", "DeepSeek"),
("VOLCENGINE_API_KEY", "Volcano Engine"),
("GEMINI_API_KEY", "Gemini"),
("GOOGLE_API_KEY", "Gemini"),
("GROQ_API_KEY", "Groq"),
Expand Down
12 changes: 12 additions & 0 deletions crates/openfang-cli/src/tui/screens/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ const PROVIDERS: &[ProviderInfo] = &[
default_model: "qwen-plus",
needs_key: true,
},
ProviderInfo {
name: "volcengine_coding",
env_var: "VOLCENGINE_API_KEY",
default_model: "ark-code-latest",
needs_key: true,
},
ProviderInfo {
name: "volcengine",
env_var: "VOLCENGINE_API_KEY",
default_model: "doubao-seed-1-6-251015",
needs_key: true,
},
ProviderInfo {
name: "perplexity",
env_var: "PERPLEXITY_API_KEY",
Expand Down
21 changes: 15 additions & 6 deletions crates/openfang-kernel/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1759,8 +1759,11 @@ impl OpenFangKernel {

// Look up model's actual context window from the catalog
let ctx_window = self.model_catalog.read().ok().and_then(|cat| {
cat.find_model(&entry.manifest.model.model)
.map(|m| m.context_window as usize)
cat.find_model_for_provider(
&entry.manifest.model.model,
&entry.manifest.model.provider,
)
.map(|m| m.context_window as usize)
});

let (tx, rx) = tokio::sync::mpsc::channel::<StreamEvent>(64);
Expand Down Expand Up @@ -2043,12 +2046,14 @@ impl OpenFangKernel {

// Persist usage to database (same as non-streaming path)
let model = &manifest.model.model;
let cost = MeteringEngine::estimate_cost_with_catalog(
let provider = &manifest.model.provider;
let cost = MeteringEngine::estimate_cost_with_catalog_for_provider(
&kernel_clone
.model_catalog
.read()
.unwrap_or_else(|e| e.into_inner()),
model,
provider,
result.total_usage.input_tokens,
result.total_usage.output_tokens,
);
Expand Down Expand Up @@ -2528,7 +2533,7 @@ impl OpenFangKernel {

// Look up model's actual context window from the catalog
let ctx_window = self.model_catalog.read().ok().and_then(|cat| {
cat.find_model(&manifest.model.model)
cat.find_model_for_provider(&manifest.model.model, &manifest.model.provider)
.map(|m| m.context_window as usize)
});

Expand Down Expand Up @@ -2600,9 +2605,11 @@ impl OpenFangKernel {

// Record usage in the metering engine (uses catalog pricing as single source of truth)
let model = &manifest.model.model;
let cost = MeteringEngine::estimate_cost_with_catalog(
let provider = &manifest.model.provider;
let cost = MeteringEngine::estimate_cost_with_catalog_for_provider(
&self.model_catalog.read().unwrap_or_else(|e| e.into_inner()),
model,
provider,
result.total_usage.input_tokens,
result.total_usage.output_tokens,
);
Expand Down Expand Up @@ -3094,9 +3101,11 @@ impl OpenFangKernel {
.unwrap_or((0, 0));

let model = &entry.manifest.model.model;
let cost = MeteringEngine::estimate_cost_with_catalog(
let provider = &entry.manifest.model.provider;
let cost = MeteringEngine::estimate_cost_with_catalog_for_provider(
&self.model_catalog.read().unwrap_or_else(|e| e.into_inner()),
model,
provider,
input_tokens,
output_tokens,
);
Expand Down
19 changes: 19 additions & 0 deletions crates/openfang-kernel/src/metering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@ impl MeteringEngine {
input_cost + output_cost
}

/// Like `estimate_cost_with_catalog` but scopes the catalog lookup to `provider`.
///
/// Prevents cross-provider pricing errors when multiple providers share the same
/// short model name (e.g. `minimax-m2.5` exists under both MiniMax and Volcengine).
pub fn estimate_cost_with_catalog_for_provider(
catalog: &openfang_runtime::model_catalog::ModelCatalog,
model: &str,
provider: &str,
input_tokens: u64,
output_tokens: u64,
) -> f64 {
let (input_per_m, output_per_m) = catalog
.pricing_for_provider(model, provider)
.unwrap_or((1.0, 3.0));
let input_cost = (input_tokens as f64 / 1_000_000.0) * input_per_m;
let output_cost = (output_tokens as f64 / 1_000_000.0) * output_per_m;
input_cost + output_cost
}

/// Clean up old usage records.
pub fn cleanup(&self, days: u32) -> OpenFangResult<usize> {
self.store.cleanup_old(days)
Expand Down
5 changes: 4 additions & 1 deletion crates/openfang-runtime/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ pub fn detect_available_provider() -> Option<(&'static str, &'static str, &'stat
("gemini", "gemini-2.5-flash", "GEMINI_API_KEY"),
("groq", "llama-3.3-70b-versatile", "GROQ_API_KEY"),
("deepseek", "deepseek-chat", "DEEPSEEK_API_KEY"),
("volcengine_coding", "ark-code-latest", "VOLCENGINE_API_KEY"),
(
"openrouter",
"openrouter/google/gemini-2.5-flash",
Expand Down Expand Up @@ -584,6 +585,7 @@ pub fn known_providers() -> &'static [&'static str] {
"kimi_coding",
"qianfan",
"volcengine",
"volcengine_coding",
"chutes",
"venice",
"nvidia",
Expand Down Expand Up @@ -689,13 +691,14 @@ mod tests {
assert!(providers.contains(&"kimi_coding"));
assert!(providers.contains(&"qianfan"));
assert!(providers.contains(&"volcengine"));
assert!(providers.contains(&"volcengine_coding"));
assert!(providers.contains(&"chutes"));
assert!(providers.contains(&"nvidia"));
assert!(providers.contains(&"codex"));
assert!(providers.contains(&"claude-code"));
assert!(providers.contains(&"qwen-code"));
assert!(providers.contains(&"azure"));
assert_eq!(providers.len(), 37);
assert_eq!(providers.len(), 38);
}

#[test]
Expand Down
11 changes: 5 additions & 6 deletions crates/openfang-runtime/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use rmcp::service::RunningService;
use rmcp::{RoleClient, ServiceExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -307,11 +306,11 @@ impl McpConnection {
}
}

let config = StreamableHttpClientTransportConfig {
uri: Arc::from(url),
custom_headers,
..Default::default()
};
// `StreamableHttpClientTransportConfig` is `#[non_exhaustive]`,
// so we must use its builder-style constructors instead of a
// struct literal with `..Default`.
let config = StreamableHttpClientTransportConfig::with_uri(url)
.custom_headers(custom_headers);

let transport = StreamableHttpClientTransport::from_config(config);

Expand Down
Loading