feat: add image support across all endpoints (responses, completions, gemini)
This commit is contained in:
@@ -4,9 +4,12 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{sse::Event, IntoResponse, Json},
|
||||
};
|
||||
use base64::Engine;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
|
||||
use super::types::{ErrorDetail, ErrorResponse};
|
||||
use crate::proto::ImageData;
|
||||
|
||||
pub(crate) fn err_response(
|
||||
status: StatusCode,
|
||||
@@ -34,3 +37,55 @@ pub(crate) fn responses_sse_event(event_type: &str, data: serde_json::Value) ->
|
||||
.event(event_type)
|
||||
.data(serde_json::to_string(&data).unwrap())
|
||||
}
|
||||
|
||||
// ─── Image extraction ────────────────────────────────────────────────────────
|
||||
|
||||
/// Parse a base64 data URI like `data:image/png;base64,iVBOR...` into ImageData.
|
||||
/// Also accepts plain URLs (returns None — we only support inline base64).
|
||||
pub(crate) fn parse_data_uri(url: &str) -> Option<ImageData> {
|
||||
// data:image/png;base64,<data>
|
||||
let rest = url.strip_prefix("data:")?;
|
||||
let (header, b64) = rest.split_once(";base64,")?;
|
||||
let mime_type = header.to_string();
|
||||
|
||||
match base64::engine::general_purpose::STANDARD.decode(b64) {
|
||||
Ok(data) => {
|
||||
tracing::info!(mime = %mime_type, size = data.len(), "Decoded inline image");
|
||||
Some(ImageData { mime_type, data })
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to decode base64 image data");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract an image from an OpenAI content array item.
|
||||
///
|
||||
/// Supports:
|
||||
/// - Chat Completions: `{"type": "image_url", "image_url": {"url": "data:..."}}`
|
||||
/// - Responses API: `{"type": "input_image", "image_url": "data:..."}` or
|
||||
/// `{"type": "input_image", "url": "data:..."}`
|
||||
pub(crate) fn extract_image_from_content(item: &serde_json::Value) -> Option<ImageData> {
|
||||
let item_type = item["type"].as_str().unwrap_or("");
|
||||
|
||||
match item_type {
|
||||
// OpenAI Chat Completions format
|
||||
"image_url" => {
|
||||
let url = item["image_url"]["url"].as_str()?;
|
||||
parse_data_uri(url)
|
||||
}
|
||||
// OpenAI Responses API format
|
||||
"input_image" => {
|
||||
let url = item["image_url"].as_str()
|
||||
.or_else(|| item["url"].as_str())?;
|
||||
parse_data_uri(url)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the first image from a content array (Value::Array of content parts).
|
||||
pub(crate) fn extract_first_image(content: &serde_json::Value) -> Option<ImageData> {
|
||||
content.as_array()?.iter().find_map(extract_image_from_content)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user