1
0
forked from boos/BoosAPI

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:
boos
2026-05-18 09:11:29 +08:00
parent 725f44699a
commit 5d29f03d50
1165 changed files with 271 additions and 323571 deletions
+411
View File
@@ -0,0 +1,411 @@
use super::retry_policy::classify_failure;
use super::types::{
AttemptFailure, AttemptFailureAction, AttemptResult, ExecuteAttemptContext, ExecuteInput,
ExecuteResult, FailureInfo, FailureReason, SelectedChannel, SuccessMetrics,
};
use futures::future::BoxFuture;
use serde_json::Value;
use std::sync::Arc;
const MAX_ATTEMPTS: usize = 5;
pub struct ProxyConductor {
pub select_channel: Arc<
dyn Fn(String, Option<Value>) -> BoxFuture<'static, Option<SelectedChannel>> + Send + Sync,
>,
pub select_next_channel: Arc<
dyn Fn(String, Vec<i64>, Option<Value>) -> BoxFuture<'static, Option<SelectedChannel>>
+ Send
+ Sync,
>,
pub record_success: Arc<dyn Fn(i64, SuccessMetrics) -> BoxFuture<'static, ()> + Send + Sync>,
pub record_failure: Arc<dyn Fn(i64, FailureInfo) -> BoxFuture<'static, ()> + Send + Sync>,
pub refresh_auth: Option<
Arc<
dyn Fn(SelectedChannel, FailureInfo) -> BoxFuture<'static, Option<SelectedChannel>>
+ Send
+ Sync,
>,
>,
}
impl ProxyConductor {
pub async fn execute(&self, input: ExecuteInput) -> ExecuteResult {
let mut attempts = 0;
let mut exclude_channel_ids = Vec::new();
let mut selected = match (self.select_channel)(
input.requested_model.clone(),
input.downstream_policy.clone(),
)
.await
{
Some(selected) => selected,
None => {
return ExecuteResult::Failure {
reason: FailureReason::NoChannel,
selected: None,
status: None,
raw_error_text: None,
attempts,
};
}
};
loop {
if attempts >= MAX_ATTEMPTS {
return exhausted_failure(
selected,
None,
None,
attempts,
input.on_terminal_failure.as_deref(),
)
.await;
}
let ctx = ExecuteAttemptContext {
selected: selected.clone(),
attempt_index: attempts,
exclude_channel_ids: exclude_channel_ids.clone(),
};
attempts += 1;
match (input.attempt_fn)(ctx).await {
AttemptResult::Success {
response,
latency_ms,
cost,
} => {
(self.record_success)(
selected.channel_id,
SuccessMetrics {
latency_ms,
cost,
metadata: Default::default(),
},
)
.await;
return ExecuteResult::Success {
selected,
response,
attempts,
};
}
AttemptResult::Failure {
action,
status,
raw_error_text,
} => {
let classified = if matches!(action, AttemptFailureAction::Stop) {
AttemptFailureAction::Stop
} else {
classify_failure(status, raw_error_text.as_deref())
};
let info = FailureInfo {
action: classified.as_str().to_string(),
status,
raw_error_text: raw_error_text.clone(),
};
(self.record_failure)(selected.channel_id, info.clone()).await;
match classified {
AttemptFailureAction::RetrySameChannel => {
continue;
}
AttemptFailureAction::RefreshAuth => {
if let Some(refresh_auth) = &self.refresh_auth {
if let Some(refreshed) =
refresh_auth(selected.clone(), info.clone()).await
{
selected = refreshed;
continue;
}
}
exclude_channel_ids.push(selected.channel_id);
match (self.select_next_channel)(
input.requested_model.clone(),
exclude_channel_ids.clone(),
input.downstream_policy.clone(),
)
.await
{
Some(next) => {
selected = next;
}
None => {
return terminal_failure(
selected,
FailureReason::Failed,
status,
raw_error_text,
attempts,
input.on_terminal_failure.as_deref(),
&info,
)
.await;
}
}
}
AttemptFailureAction::Failover => {
exclude_channel_ids.push(selected.channel_id);
match (self.select_next_channel)(
input.requested_model.clone(),
exclude_channel_ids.clone(),
input.downstream_policy.clone(),
)
.await
{
Some(next) => {
selected = next;
}
None => {
return terminal_failure(
selected,
FailureReason::Failed,
status,
raw_error_text,
attempts,
input.on_terminal_failure.as_deref(),
&info,
)
.await;
}
}
}
AttemptFailureAction::Terminal => {
return terminal_failure(
selected,
FailureReason::Terminal,
status,
raw_error_text,
attempts,
input.on_terminal_failure.as_deref(),
&info,
)
.await;
}
AttemptFailureAction::Stop => {
return terminal_failure(
selected,
FailureReason::Failed,
status,
raw_error_text,
attempts,
input.on_terminal_failure.as_deref(),
&info,
)
.await;
}
}
}
}
}
}
}
async fn terminal_failure(
selected: SelectedChannel,
reason: FailureReason,
status: Option<u16>,
raw_error_text: Option<String>,
attempts: usize,
on_terminal_failure: Option<
&(dyn Fn(SelectedChannel, AttemptFailure) -> BoxFuture<'static, ()> + Send + Sync),
>,
info: &FailureInfo,
) -> ExecuteResult {
if let Some(callback) = on_terminal_failure {
callback(selected.clone(), AttemptFailure::from(info)).await;
}
ExecuteResult::Failure {
reason,
selected: Some(selected),
status,
raw_error_text,
attempts,
}
}
async fn exhausted_failure(
selected: SelectedChannel,
status: Option<u16>,
raw_error_text: Option<String>,
attempts: usize,
on_terminal_failure: Option<
&(dyn Fn(SelectedChannel, AttemptFailure) -> BoxFuture<'static, ()> + Send + Sync),
>,
) -> ExecuteResult {
let info = FailureInfo {
action: AttemptFailureAction::Stop.as_str().to_string(),
status,
raw_error_text: raw_error_text.clone(),
};
terminal_failure(
selected,
FailureReason::Failed,
status,
raw_error_text,
attempts,
on_terminal_failure,
&info,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt;
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
fn channel(id: i64) -> SelectedChannel {
SelectedChannel {
channel_id: id,
route_id: Some(10),
site_id: 20,
account_id: Some(30),
base_url: "https://example.test/v1".into(),
api_key: "key".into(),
model: "gpt-5.5".into(),
actual_model: "gpt-5.5".into(),
token_name: None,
token_value: None,
}
}
#[tokio::test]
async fn retries_same_channel_then_succeeds() {
let attempts_seen = Arc::new(AtomicUsize::new(0));
let recorded_failures = Arc::new(AtomicUsize::new(0));
let recorded_successes = Arc::new(AtomicUsize::new(0));
let conductor = ProxyConductor {
select_channel: Arc::new(|_, _| async { Some(channel(1)) }.boxed()),
select_next_channel: Arc::new(|_, _, _| async { Some(channel(2)) }.boxed()),
record_success: {
let recorded_successes = Arc::clone(&recorded_successes);
Arc::new(move |_, _| {
let recorded_successes = Arc::clone(&recorded_successes);
async move {
recorded_successes.fetch_add(1, Ordering::SeqCst);
}
.boxed()
})
},
record_failure: {
let recorded_failures = Arc::clone(&recorded_failures);
Arc::new(move |_, _| {
let recorded_failures = Arc::clone(&recorded_failures);
async move {
recorded_failures.fetch_add(1, Ordering::SeqCst);
}
.boxed()
})
},
refresh_auth: None,
};
let result = conductor
.execute(ExecuteInput {
requested_model: "gpt-5.5".into(),
downstream_policy: None,
attempt_fn: Box::new({
let attempts_seen = Arc::clone(&attempts_seen);
move |_| {
let attempt = attempts_seen.fetch_add(1, Ordering::SeqCst);
async move {
if attempt == 0 {
AttemptResult::Failure {
action: AttemptFailureAction::RetrySameChannel,
status: Some(429),
raw_error_text: Some("rate limited".into()),
}
} else {
AttemptResult::Success {
response: json!({"ok": true}),
latency_ms: Some(12),
cost: Some(0.1),
}
}
}
.boxed()
}
}),
on_terminal_failure: None,
})
.await;
match result {
ExecuteResult::Success {
selected, attempts, ..
} => {
assert_eq!(selected.channel_id, 1);
assert_eq!(attempts, 2);
}
other => panic!("expected success, got {other:?}"),
}
assert_eq!(recorded_failures.load(Ordering::SeqCst), 1);
assert_eq!(recorded_successes.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn failover_selects_next_channel() {
let conductor = ProxyConductor {
select_channel: Arc::new(|_, _| async { Some(channel(1)) }.boxed()),
select_next_channel: Arc::new(|_, excluded, _| {
async move {
assert_eq!(excluded, vec![1]);
Some(channel(2))
}
.boxed()
}),
record_success: Arc::new(|_, _| async {}.boxed()),
record_failure: Arc::new(|_, _| async {}.boxed()),
refresh_auth: None,
};
let attempts_seen = Arc::new(AtomicUsize::new(0));
let result = conductor
.execute(ExecuteInput {
requested_model: "gpt-5.5".into(),
downstream_policy: None,
attempt_fn: Box::new({
let attempts_seen = Arc::clone(&attempts_seen);
move |ctx| {
let attempt = attempts_seen.fetch_add(1, Ordering::SeqCst);
async move {
if attempt == 0 {
assert_eq!(ctx.selected.channel_id, 1);
AttemptResult::Failure {
action: AttemptFailureAction::Failover,
status: Some(500),
raw_error_text: None,
}
} else {
assert_eq!(ctx.selected.channel_id, 2);
AttemptResult::Success {
response: json!({"ok": true}),
latency_ms: None,
cost: None,
}
}
}
.boxed()
}
}),
on_terminal_failure: None,
})
.await;
match result {
ExecuteResult::Success {
selected, attempts, ..
} => {
assert_eq!(selected.channel_id, 2);
assert_eq!(attempts, 2);
}
other => panic!("expected success, got {other:?}"),
}
}
}
+12
View File
@@ -0,0 +1,12 @@
pub mod conductor;
pub mod retry_policy;
pub mod types;
pub use conductor::ProxyConductor;
pub use retry_policy::{
classify_failure, is_terminal, should_failover, should_refresh_auth, should_retry_same_channel,
};
pub use types::{
AttemptFailure, AttemptFailureAction, AttemptResult, ExecuteAttemptContext, ExecuteInput,
ExecuteResult, FailureInfo, FailureReason, SelectedChannel, SuccessMetrics,
};
+82
View File
@@ -0,0 +1,82 @@
use super::types::AttemptFailureAction;
pub fn classify_failure(status: Option<u16>, _error_text: Option<&str>) -> AttemptFailureAction {
match status {
Some(429 | 503) => AttemptFailureAction::RetrySameChannel,
Some(401) => AttemptFailureAction::RefreshAuth,
Some(code) if (500..600).contains(&code) => AttemptFailureAction::Failover,
Some(code) if (400..500).contains(&code) => AttemptFailureAction::Terminal,
_ => AttemptFailureAction::Failover,
}
}
pub fn is_terminal(action: AttemptFailureAction) -> bool {
matches!(action, AttemptFailureAction::Terminal)
}
pub fn should_retry_same_channel(action: AttemptFailureAction) -> bool {
matches!(action, AttemptFailureAction::RetrySameChannel)
}
pub fn should_refresh_auth(action: AttemptFailureAction) -> bool {
matches!(action, AttemptFailureAction::RefreshAuth)
}
pub fn should_failover(action: AttemptFailureAction) -> bool {
matches!(action, AttemptFailureAction::Failover)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_retryable_rate_limit_and_unavailable() {
assert_eq!(
classify_failure(Some(429), None),
AttemptFailureAction::RetrySameChannel
);
assert_eq!(
classify_failure(Some(503), Some("busy")),
AttemptFailureAction::RetrySameChannel
);
}
#[test]
fn classifies_auth_server_and_terminal_failures() {
assert_eq!(
classify_failure(Some(401), None),
AttemptFailureAction::RefreshAuth
);
assert_eq!(
classify_failure(Some(500), None),
AttemptFailureAction::Failover
);
assert_eq!(
classify_failure(Some(502), None),
AttemptFailureAction::Failover
);
assert_eq!(
classify_failure(Some(400), None),
AttemptFailureAction::Terminal
);
assert_eq!(
classify_failure(Some(404), None),
AttemptFailureAction::Terminal
);
assert_eq!(
classify_failure(None, Some("timeout")),
AttemptFailureAction::Failover
);
}
#[test]
fn helper_predicates_match_actions() {
assert!(is_terminal(AttemptFailureAction::Terminal));
assert!(should_retry_same_channel(
AttemptFailureAction::RetrySameChannel
));
assert!(should_refresh_auth(AttemptFailureAction::RefreshAuth));
assert!(should_failover(AttemptFailureAction::Failover));
}
}
+124
View File
@@ -0,0 +1,124 @@
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SelectedChannel {
pub channel_id: i64,
pub route_id: Option<i64>,
pub site_id: i64,
pub account_id: Option<i64>,
pub base_url: String,
pub api_key: String,
pub model: String,
pub actual_model: String,
pub token_name: Option<String>,
pub token_value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttemptFailureAction {
RetrySameChannel,
RefreshAuth,
Failover,
Terminal,
Stop,
}
#[derive(Debug)]
pub enum AttemptResult {
Success {
response: Value,
latency_ms: Option<u64>,
cost: Option<f64>,
},
Failure {
action: AttemptFailureAction,
status: Option<u16>,
raw_error_text: Option<String>,
},
}
pub struct ExecuteInput {
pub requested_model: String,
pub downstream_policy: Option<Value>,
pub attempt_fn: Box<dyn Fn(ExecuteAttemptContext) -> BoxFuture<'static, AttemptResult> + Send>,
pub on_terminal_failure: Option<
Box<dyn Fn(SelectedChannel, AttemptFailure) -> BoxFuture<'static, ()> + Send + Sync>,
>,
}
#[derive(Debug, Clone)]
pub struct ExecuteAttemptContext {
pub selected: SelectedChannel,
pub attempt_index: usize,
pub exclude_channel_ids: Vec<i64>,
}
#[derive(Debug)]
pub enum ExecuteResult {
Success {
selected: SelectedChannel,
response: Value,
attempts: usize,
},
Failure {
reason: FailureReason,
selected: Option<SelectedChannel>,
status: Option<u16>,
raw_error_text: Option<String>,
attempts: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureReason {
NoChannel,
Failed,
Terminal,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SuccessMetrics {
pub latency_ms: Option<u64>,
pub cost: Option<f64>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailureInfo {
pub action: String,
pub status: Option<u16>,
pub raw_error_text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttemptFailure {
pub status: Option<u16>,
pub raw_error_text: Option<String>,
pub action: String,
}
impl From<&FailureInfo> for AttemptFailure {
fn from(value: &FailureInfo) -> Self {
Self {
status: value.status,
raw_error_text: value.raw_error_text.clone(),
action: value.action.clone(),
}
}
}
impl AttemptFailureAction {
pub fn as_str(self) -> &'static str {
match self {
Self::RetrySameChannel => "retry_same_channel",
Self::RefreshAuth => "refresh_auth",
Self::Failover => "failover",
Self::Terminal => "terminal",
Self::Stop => "stop",
}
}
}