1
0
forked from boos/BoosAPI

feat: merge opus comfyui and login updates

This commit is contained in:
boos
2026-05-21 16:32:04 +02:00
parent c16479da7c
commit 9c84759498
105 changed files with 20160 additions and 1034 deletions
+77
View File
@@ -209,3 +209,80 @@ pub async fn execute_image_generation(
Ok(json)
}
/// One field forwarded to the upstream `/images/edits` endpoint.
///
/// `bytes` is None → text field; Some → file field. We don't try to
/// detect the difference at the HTTP layer because OpenAI's spec already
/// pins which fields are files (`image`, `image[]`, `mask`) — the route
/// handler classifies them based on the field's content-type/filename.
pub struct MultipartField {
pub name: String,
pub bytes: Option<Vec<u8>>,
pub filename: Option<String>,
pub content_type: Option<String>,
/// Text value for non-file fields. Ignored when `bytes` is Some.
pub text: Option<String>,
}
/// Execute an image edits request (`POST /v1/images/edits`).
///
/// Unlike /images/generations this is `multipart/form-data` (the request
/// carries one or more image binaries plus the prompt). We rebuild a
/// fresh upstream multipart form from the parsed fields rather than
/// streaming the raw body — that lets us swap out the `model` field with
/// the route-resolved upstream model and skip any client-side fields
/// that don't belong on the wire.
pub async fn execute_image_edit(
_state: AppState,
route: UpstreamRoute,
fields: Vec<MultipartField>,
) -> AppResult<Value> {
use reqwest::multipart::{Form, Part};
let client = http_client();
let upstream_url = format!("{}/images/edits", route.base_url.trim_end_matches('/'));
let mut form = Form::new();
for field in fields {
if let Some(bytes) = field.bytes {
let mut part = Part::bytes(bytes);
if let Some(filename) = field.filename {
part = part.file_name(filename);
}
if let Some(ct) = field.content_type {
part = part
.mime_str(&ct)
.map_err(|e| AppError::BadRequest(format!("invalid content-type: {e}")))?;
}
form = form.part(field.name, part);
} else {
form = form.text(field.name, field.text.unwrap_or_default());
}
}
let resp = client
.post(&upstream_url)
.header("Authorization", format!("Bearer {}", route.api_key))
.multipart(form)
.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)
}
+16
View File
@@ -376,6 +376,22 @@ mod tests {
.execute(&pool)
.await
.unwrap();
// select_channel_for_route's COALESCE subquery joins through
// site_api_endpoints. The table must exist or the SELECT errors
// out — we leave it empty and the COALESCE falls back to sites.url.
sqlx::query(
"CREATE TABLE site_api_endpoints (
id INTEGER PRIMARY KEY,
site_id INTEGER NOT NULL,
url TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
cooldown_until TEXT
)",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO sites (id, name, url, api_key) VALUES (1, 'OpenAI', 'https://api.example/v1', 'site-key')")
.execute(&pool)