forked from boos/BoosAPI
feat: merge opus comfyui and login updates
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user