feat: add image support across all endpoints (responses, completions, gemini)

This commit is contained in:
Nikketryhard
2026-02-15 17:25:33 -06:00
parent ca9f808ee3
commit 976c44fdd4
6 changed files with 168 additions and 33 deletions

View File

@@ -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)
}