1
0
forked from boos/BoosAPI

feat: add workflow studio and postgres support

This commit is contained in:
boos
2026-05-25 16:03:07 +08:00
parent 7cdb02ef1d
commit cb35d5fc6e
96 changed files with 12414 additions and 15548 deletions
+1
View File
@@ -267,6 +267,7 @@ mod tests {
route_id: Some(10),
site_id: 20,
account_id: Some(30),
account_unit_cost: None,
base_url: "https://example.test/v1".into(),
api_key: "key".into(),
model: "gpt-5.5".into(),
+1
View File
@@ -11,6 +11,7 @@ pub struct SelectedChannel {
pub route_id: Option<i64>,
pub site_id: i64,
pub account_id: Option<i64>,
pub account_unit_cost: Option<f64>,
pub base_url: String,
pub api_key: String,
pub model: String,
+28 -24
View File
@@ -76,6 +76,23 @@ fn looks_like_sse(text: &str) -> bool {
text.trim_start().starts_with("data:")
}
fn openai_compatible_base_url(base_url: &str) -> String {
let trimmed = base_url.trim().trim_end_matches('/');
if trimmed.ends_with("/v1") || trimmed.contains("/v1/") {
trimmed.to_string()
} else {
format!("{trimmed}/v1")
}
}
fn upstream_url(base_url: &str, path_suffix: &str) -> String {
format!(
"{}/{}",
openai_compatible_base_url(base_url),
path_suffix.trim_start_matches('/')
)
}
fn aggregate_chat_sse(text: &str, fallback_model: &str) -> Option<Value> {
let mut id: Option<String> = None;
let mut created: Option<i64> = None;
@@ -162,7 +179,7 @@ pub async fn execute_chat_completions(
body: Value,
) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!("{}/chat/completions", route.base_url.trim_end_matches('/'));
let upstream_url = upstream_url(&route.base_url, "/chat/completions");
let resp = client
.post(&upstream_url)
@@ -279,8 +296,8 @@ pub async fn execute_anthropic_messages_stream(
let mut stream = resp.bytes_stream();
use futures::StreamExt;
while let Some(chunk_result) = stream.next().await {
let bytes = chunk_result
.map_err(|e| AppError::UpstreamError(format!("Stream error: {e}")))?;
let bytes =
chunk_result.map_err(|e| AppError::UpstreamError(format!("Stream error: {e}")))?;
let text = String::from_utf8_lossy(&bytes).to_string();
frames.extend(normalize_anthropic_sse_frames(&text));
}
@@ -328,7 +345,9 @@ fn normalize_anthropic_sse_frame(frame: &str) -> Option<String> {
let choice = &event.choices[0];
if choice.delta.role.is_some() {
let id = event.id.unwrap_or_else(|| format!("msg_{}", uuid::Uuid::new_v4().simple()));
let id = event
.id
.unwrap_or_else(|| format!("msg_{}", uuid::Uuid::new_v4().simple()));
return Some(format!(
"event: message_start\ndata: {}\n\n",
serde_json::json!({
@@ -392,11 +411,7 @@ pub async fn execute_streaming_request(
body: Value,
) -> AppResult<Vec<String>> {
let client = http_client();
let upstream_url = format!(
"{}/{}",
route.base_url.trim_end_matches('/'),
path_suffix.trim_start_matches('/')
);
let upstream_url = upstream_url(&route.base_url, path_suffix);
let resp = client
.post(&upstream_url)
@@ -448,11 +463,7 @@ pub async fn execute_proxy_request(
body: Value,
) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!(
"{}/{}",
route.base_url.trim_end_matches('/'),
path_suffix.trim_start_matches('/')
);
let upstream_url = upstream_url(&route.base_url, path_suffix);
let resp = client
.post(&upstream_url)
@@ -483,11 +494,7 @@ pub async fn execute_proxy_request(
/// Generic GET proxy request to an upstream endpoint path.
pub async fn execute_proxy_get(route: UpstreamRoute, path_suffix: &str) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!(
"{}/{}",
route.base_url.trim_end_matches('/'),
path_suffix.trim_start_matches('/')
);
let upstream_url = upstream_url(&route.base_url, path_suffix);
let resp = client
.get(&upstream_url)
@@ -602,10 +609,7 @@ pub async fn execute_image_generation(
body: Value,
) -> AppResult<Value> {
let client = http_client();
let upstream_url = format!(
"{}/images/generations",
route.base_url.trim_end_matches('/')
);
let upstream_url = upstream_url(&route.base_url, "/images/generations");
let resp = client
.post(&upstream_url)
@@ -664,7 +668,7 @@ pub async fn execute_image_edit(
use reqwest::multipart::{Form, Part};
let client = http_client();
let upstream_url = format!("{}/images/edits", route.base_url.trim_end_matches('/'));
let upstream_url = upstream_url(&route.base_url, "/images/edits");
let mut form = Form::new();
for field in fields {
+3
View File
@@ -29,6 +29,7 @@ struct ChannelRow {
site_api_key: Option<String>,
account_access_token: String,
account_api_token: Option<String>,
account_unit_cost: Option<f64>,
token_name: Option<String>,
token_value: Option<String>,
source_model: Option<String>,
@@ -148,6 +149,7 @@ impl TokenRouter {
s.api_key AS site_api_key,
a.access_token AS account_access_token,
a.api_token AS account_api_token,
a.unit_cost AS account_unit_cost,
at.name AS token_name,
at.token AS token_value,
rc.source_model AS source_model
@@ -198,6 +200,7 @@ impl TokenRouter {
route_id: Some(row.route_id),
site_id: row.site_id,
account_id: Some(row.account_id),
account_unit_cost: row.account_unit_cost,
base_url: row.base_url,
api_key: api_key
.trim()