refactor: purge Node.js, Rust is the one true backend
- Moved boosapi-rs/* to repo root - Deleted all Node.js source (src/server/, src/web/, scripts/) - Deleted node_modules, package.json, tsconfig, vite.config - Updated .gitignore and AGENTS.md for Rust-only stack - Fixed static file path for frontend dist/web/ - Dockerfile stays the same (already Rust-only) - Kept dist/web/ for Rust static serving
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// Provider profiles & registry
|
||||
pub mod registry;
|
||||
pub mod types;
|
||||
|
||||
pub use registry::ProviderRegistry;
|
||||
pub use types::{ProviderCategory, ProviderProfile, RouteResolution};
|
||||
@@ -0,0 +1,134 @@
|
||||
// Provider registry — holds all configured upstream provider profiles
|
||||
use crate::proxy::providers::types::{ProviderCategory, ProviderProfile, RouteResolution};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Registry of all available provider profiles
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderRegistry {
|
||||
providers: HashMap<String, ProviderProfile>,
|
||||
}
|
||||
|
||||
impl ProviderRegistry {
|
||||
/// Create a new registry with default providers
|
||||
pub fn new() -> Self {
|
||||
let mut registry = Self {
|
||||
providers: HashMap::new(),
|
||||
};
|
||||
registry.register_defaults();
|
||||
registry
|
||||
}
|
||||
|
||||
/// Register the built-in default providers
|
||||
fn register_defaults(&mut self) {
|
||||
// OpenAI
|
||||
self.register(ProviderProfile::openai_compatible(
|
||||
"openai",
|
||||
"OpenAI",
|
||||
"https://api.openai.com/v1",
|
||||
));
|
||||
// Anthropic
|
||||
self.register(ProviderProfile::anthropic(
|
||||
"anthropic",
|
||||
"Anthropic",
|
||||
"https://api.anthropic.com/v1",
|
||||
));
|
||||
// Google Gemini
|
||||
self.register(ProviderProfile::gemini(
|
||||
"gemini",
|
||||
"Google Gemini",
|
||||
"https://generativelanguage.googleapis.com/v1beta",
|
||||
));
|
||||
// Codex
|
||||
self.register(ProviderProfile {
|
||||
id: "codex".into(),
|
||||
name: "OpenAI Codex".into(),
|
||||
category: ProviderCategory::Codex,
|
||||
base_url: "https://api.openai.com/v1".into(),
|
||||
default_model: Some("codex".into()),
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers: HashMap::new(),
|
||||
});
|
||||
// Claude Code
|
||||
self.register(ProviderProfile {
|
||||
id: "claude".into(),
|
||||
name: "Claude Code".into(),
|
||||
category: ProviderCategory::Claude,
|
||||
base_url: "https://api.anthropic.com/v1".into(),
|
||||
default_model: Some("claude-sonnet-4".into()),
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers: HashMap::new(),
|
||||
});
|
||||
// Gemini CLI
|
||||
self.register(ProviderProfile {
|
||||
id: "gemini_cli".into(),
|
||||
name: "Gemini CLI".into(),
|
||||
category: ProviderCategory::GeminiCli,
|
||||
base_url: "https://generativelanguage.googleapis.com/v1beta".into(),
|
||||
default_model: Some("gemini-2.0-flash".into()),
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers: HashMap::new(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Register a new provider profile
|
||||
pub fn register(&mut self, profile: ProviderProfile) {
|
||||
self.providers.insert(profile.id.clone(), profile);
|
||||
}
|
||||
|
||||
/// Get a provider by ID
|
||||
pub fn get(&self, id: &str) -> Option<&ProviderProfile> {
|
||||
self.providers.get(id)
|
||||
}
|
||||
|
||||
/// List all registered providers
|
||||
pub fn list(&self) -> Vec<&ProviderProfile> {
|
||||
self.providers.values().collect()
|
||||
}
|
||||
|
||||
/// Remove a provider by ID
|
||||
pub fn remove(&mut self, id: &str) -> Option<ProviderProfile> {
|
||||
self.providers.remove(id)
|
||||
}
|
||||
|
||||
/// Resolve a model name to an upstream route.
|
||||
/// Tries to match the model to a provider category, falls back to first OpenAI-compatible provider.
|
||||
pub fn resolve(&self, model: &str, api_key: &str) -> Option<RouteResolution> {
|
||||
// First try to find a provider that supports this model
|
||||
for provider in self.providers.values() {
|
||||
if provider.supports_model(model) {
|
||||
let base_url = provider.base_url.clone();
|
||||
return Some(RouteResolution {
|
||||
provider: provider.clone(),
|
||||
api_key: api_key.to_string(),
|
||||
model: model.to_string(),
|
||||
base_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the first OpenAI-compatible provider
|
||||
self.providers
|
||||
.values()
|
||||
.find(|p| {
|
||||
matches!(
|
||||
p.category,
|
||||
ProviderCategory::OpenAi | ProviderCategory::GenericOpenAi
|
||||
)
|
||||
})
|
||||
.map(|provider| RouteResolution {
|
||||
provider: provider.clone(),
|
||||
api_key: api_key.to_string(),
|
||||
model: model.to_string(),
|
||||
base_url: provider.base_url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProviderRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Provider profile types — upstream API provider definitions
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Provider category — determines request transformation and routing behavior
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ProviderCategory {
|
||||
/// OpenAI-compatible chat/completions API
|
||||
OpenAi,
|
||||
/// Anthropic Messages API
|
||||
Anthropic,
|
||||
/// Google Gemini API
|
||||
Gemini,
|
||||
/// Codex CLI / Responses API
|
||||
Codex,
|
||||
/// Claude Code CLI
|
||||
Claude,
|
||||
/// Gemini CLI
|
||||
GeminiCli,
|
||||
/// Antigravity / assistant-style API
|
||||
Antigravity,
|
||||
/// Generic OpenAI-compatible proxy (new-api, one-api, etc.)
|
||||
GenericOpenAi,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProviderCategory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::OpenAi => write!(f, "openai"),
|
||||
Self::Anthropic => write!(f, "anthropic"),
|
||||
Self::Gemini => write!(f, "gemini"),
|
||||
Self::Codex => write!(f, "codex"),
|
||||
Self::Claude => write!(f, "claude"),
|
||||
Self::GeminiCli => write!(f, "gemini_cli"),
|
||||
Self::Antigravity => write!(f, "antigravity"),
|
||||
Self::GenericOpenAi => write!(f, "generic_openai"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider configuration profile
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProviderProfile {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub category: ProviderCategory,
|
||||
pub base_url: String,
|
||||
pub default_model: Option<String>,
|
||||
pub requires_api_key: bool,
|
||||
pub supports_streaming: bool,
|
||||
pub headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl ProviderProfile {
|
||||
/// Create a standard OpenAI-compatible provider
|
||||
pub fn openai_compatible(id: &str, name: &str, base_url: &str) -> Self {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Content-Type".into(), "application/json".into());
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
category: ProviderCategory::GenericOpenAi,
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
default_model: None,
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Anthropic Messages API provider
|
||||
pub fn anthropic(id: &str, name: &str, base_url: &str) -> Self {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Content-Type".into(), "application/json".into());
|
||||
headers.insert("anthropic-version".into(), "2023-06-01".into());
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
category: ProviderCategory::Anthropic,
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
default_model: None,
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Gemini API provider
|
||||
pub fn gemini(id: &str, name: &str, base_url: &str) -> Self {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Content-Type".into(), "application/json".into());
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
category: ProviderCategory::Gemini,
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
default_model: None,
|
||||
requires_api_key: true,
|
||||
supports_streaming: true,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this provider handles the given model
|
||||
pub fn supports_model(&self, model: &str) -> bool {
|
||||
let model_lower = model.to_lowercase();
|
||||
match self.category {
|
||||
ProviderCategory::OpenAi | ProviderCategory::GenericOpenAi => {
|
||||
model_lower.contains("gpt")
|
||||
|| model_lower.contains("o1")
|
||||
|| model_lower.contains("o3")
|
||||
|| model_lower.contains("dall-e")
|
||||
}
|
||||
ProviderCategory::Anthropic => model_lower.contains("claude"),
|
||||
ProviderCategory::Gemini => model_lower.contains("gemini"),
|
||||
ProviderCategory::Codex => model_lower.contains("codex"),
|
||||
ProviderCategory::Claude => {
|
||||
model_lower.contains("claude") && model_lower.contains("code")
|
||||
}
|
||||
ProviderCategory::GeminiCli => {
|
||||
model_lower.contains("gemini") && model_lower.contains("cli")
|
||||
}
|
||||
ProviderCategory::Antigravity => {
|
||||
model_lower.contains("antigravity") || model_lower.contains("assistant")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of resolving a model to an upstream route
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouteResolution {
|
||||
pub provider: ProviderProfile,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
pub base_url: String,
|
||||
}
|
||||
Reference in New Issue
Block a user