forked from boos/BoosAPI
feat: add workflow studio and postgres support
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user