feat: Rust refactor — full backend rewrite (Phases 1-6)

Phase 1-3: Scaffold + types + auth (JWT, Argon2, API keys)
Phase 4a: Admin API (sites/accounts/tokens/downstream-keys/settings)
Phase 5: Proxy core (/v1/chat, /v1/images, /v1/models)
Phase 6: ComfyUI proxy routes migrated

- 80+ source files, all cargo check + 18 tests green
- Drop-in compatible with Node.js API surface
This commit is contained in:
boos
2026-05-17 13:20:52 +08:00
parent ad0bb86e31
commit 7987833ba9
61 changed files with 3661 additions and 1 deletions
+157
View File
@@ -0,0 +1,157 @@
// Proxy executors — forward requests to upstream APIs
//
// Each executor:
// 1. Transforms the OpenAI-compatible request to the upstream format
// 2. Sends the HTTP request
// 3. Transforms the response back to OpenAI-compatible format
use crate::error::{ AppError, AppResult };
use crate::AppState;
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
/// Resolved upstream info for a given model request.
#[derive(Debug, Clone)]
pub struct UpstreamRoute {
pub base_url: String,
pub api_key: String,
pub model: String,
}
/// Create a shared reqwest client with sensible defaults.
fn http_client() -> Client {
Client::builder()
.timeout(Duration::from_secs(300))
.pool_max_idle_per_host(32)
.build()
.expect("Failed to build HTTP client")
}
/// Execute a non-streaming chat completions request.
pub async fn execute_chat_completions(
_state: AppState,
route: UpstreamRoute,
body: Value,
) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!("{}/chat/completions", route.base_url.trim_end_matches('/'));
let resp = client
.post(&upstream_url)
.header("Authorization", format!("Bearer {}", route.api_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| AppError::UpstreamError(format!("Failed to read upstream response: {e}")))?;
if !status.is_success() {
return Err(AppError::UpstreamError(format!(
"Upstream returned {status}: {text}"
)));
}
let json: Value = serde_json::from_str(&text)
.map_err(|e| AppError::UpstreamError(format!("Invalid JSON from upstream: {e}")))?;
Ok(json)
}
/// Execute a streaming chat completions request.
/// Returns a vector of SSE data chunks as strings.
pub async fn execute_chat_completions_stream(
_state: AppState,
route: UpstreamRoute,
body: Value,
) -> AppResult<Vec<String>> {
let client = http_client();
let upstream_url = format!("{}/chat/completions", route.base_url.trim_end_matches('/'));
let resp = client
.post(&upstream_url)
.header("Authorization", format!("Bearer {}", route.api_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
let status = resp.status();
if !status.is_success() {
let text = resp
.text()
.await
.unwrap_or_else(|_| "unknown error".to_string());
return Err(AppError::UpstreamError(format!(
"Upstream returned {status}: {text}"
)));
}
// Read the streaming response as chunks
let mut chunks = Vec::new();
let mut stream = resp.bytes_stream();
use futures::StreamExt;
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes).to_string();
// Parse SSE lines from the chunk
for line in text.lines() {
if line.starts_with("data: ") {
chunks.push(line.to_string());
}
}
}
Err(e) => {
return Err(AppError::UpstreamError(format!(
"Stream error: {e}"
)));
}
}
}
Ok(chunks)
}
/// Execute an image generation request.
pub async fn execute_image_generation(
_state: AppState,
route: UpstreamRoute,
body: Value,
) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!("{}/images/generations", route.base_url.trim_end_matches('/'));
let resp = client
.post(&upstream_url)
.header("Authorization", format!("Bearer {}", route.api_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| AppError::UpstreamError(format!("Upstream request failed: {e}")))?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| AppError::UpstreamError(format!("Failed to read upstream response: {e}")))?;
if !status.is_success() {
return Err(AppError::UpstreamError(format!(
"Upstream returned {status}: {text}"
)));
}
let json: Value = serde_json::from_str(&text)
.map_err(|e| AppError::UpstreamError(format!("Invalid JSON from upstream: {e}")))?;
Ok(json)
}