修正预算与图片下载错误的 provider 归属

request_budget_exhausted_error 接收实际 provider,审计、日志与 Request 错误不再写死 vector-engine

download_remote_image / download_images_from_urls / resolve_reference_images / parse_reference_image_data_url 逐层透传 provider

executor 与 raw image edit 调用处传入各自 settings.provider,nanobanana 下载路径传入 VectorEngine
This commit is contained in:
2026-09-19 13:19:08 +08:00
parent 4888a0eb01
commit d3407e8910
5 changed files with 44 additions and 18 deletions
@@ -109,6 +109,7 @@ pub(crate) async fn handle_image_response(
task_id,
image_urls,
candidate_count,
provider,
request_deadline,
failure_context,
)
@@ -1,7 +1,7 @@
use std::time::{Duration, Instant};
use crate::image_provider::{
audit::build_failure_audit, constants::VECTOR_ENGINE_PROVIDER, error::PlatformImageError,
audit::build_failure_audit, error::PlatformImageError, types::ImageProvider,
};
pub(crate) fn effective_request_timeout_ms(
@@ -51,6 +51,7 @@ fn retry_delay_fits_request_deadline_at(
}
pub(crate) fn request_budget_exhausted_error(
provider: ImageProvider,
request_url: &str,
operation: &str,
image_model: Option<&'static str>,
@@ -59,9 +60,10 @@ pub(crate) fn request_budget_exhausted_error(
reference_image_count: Option<usize>,
) -> PlatformImageError {
const ERROR_SOURCE: &str = "external request deadline elapsed";
let provider_label = provider.as_str();
let message = format!("{operation}:外部图片请求执行预算已耗尽");
let audit = build_failure_audit(
VECTOR_ENGINE_PROVIDER,
provider_label,
request_url,
operation,
"request_budget",
@@ -78,7 +80,7 @@ pub(crate) fn request_budget_exhausted_error(
image_model,
);
tracing::warn!(
provider = VECTOR_ENGINE_PROVIDER,
provider = provider_label,
endpoint = %request_url,
failure_stage = "request_budget",
timeout = true,
@@ -90,7 +92,7 @@ pub(crate) fn request_budget_exhausted_error(
"ImageProvider 图片请求执行预算已耗尽"
);
PlatformImageError::Request {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider_label,
message,
endpoint: Some(request_url.to_string()),
timeout: true,
@@ -164,6 +166,7 @@ mod tests {
#[test]
fn exhausted_budget_maps_to_timeout_request_error_and_audit() {
let error = request_budget_exhausted_error(
ImageProvider::VectorEngine,
"https://vector.example/v1/images/generations",
"生成图片失败",
None,
@@ -78,6 +78,7 @@ pub async fn create_image_generation_with_model(
let resolved_references = resolve_reference_images(
http_client,
reference_images,
settings.provider,
failure_context,
settings.request_deadline,
)
@@ -116,6 +117,7 @@ pub async fn create_image_generation_with_model(
else {
return Err(finish_image_model_fallback_error(
request_budget_exhausted_error(
settings.provider,
request_url.as_str(),
failure_context,
auditable_image_model(upstream_model),
@@ -307,6 +309,7 @@ pub async fn create_nanobanana_generate_content(
effective_request_timeout_ms(settings.request_timeout_ms, settings.request_deadline)
else {
return Err(request_budget_exhausted_error(
ImageProvider::VectorEngine,
request_url.as_str(),
failure_context,
None,
@@ -550,6 +553,7 @@ pub async fn create_image_edit_with_references_and_model(
else {
return Err(finish_image_model_fallback_error(
request_budget_exhausted_error(
settings.provider,
request_url.as_str(),
failure_context,
auditable_image_model(upstream_model),
@@ -4,19 +4,21 @@ use std::time::Instant;
use crate::image_provider::{
budget::request_budget_exhausted_error,
constants::{VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES, VECTOR_ENGINE_PROVIDER},
constants::VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES,
error::PlatformImageError,
types::{DownloadedImage, GeneratedImages, ReferenceImage},
types::{DownloadedImage, GeneratedImages, ImageProvider, ReferenceImage},
};
pub async fn download_remote_image(
http_client: &reqwest::Client,
image_url: &str,
provider: ImageProvider,
) -> Result<DownloadedImage, PlatformImageError> {
let response = http_client.get(image_url).send().await.map_err(|error| {
map_simple_request_error(
format!("下载生成图片失败:{error}"),
Some(image_url.to_string()),
provider,
)
})?;
let status = response.status();
@@ -30,11 +32,12 @@ pub async fn download_remote_image(
map_simple_request_error(
format!("读取生成图片内容失败:{error}"),
Some(image_url.to_string()),
provider,
)
})?;
if !status.is_success() {
return Err(PlatformImageError::Request {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: "下载生成图片失败".to_string(),
endpoint: Some(image_url.to_string()),
timeout: false,
@@ -58,15 +61,17 @@ pub async fn download_remote_image(
async fn download_remote_image_with_deadline(
http_client: &reqwest::Client,
image_url: &str,
provider: ImageProvider,
request_deadline: Option<Instant>,
operation: &str,
) -> Result<DownloadedImage, PlatformImageError> {
let Some(request_deadline) = request_deadline else {
return download_remote_image(http_client, image_url).await;
return download_remote_image(http_client, image_url, provider).await;
};
let started_at = Instant::now();
if request_deadline <= started_at {
return Err(request_budget_exhausted_error(
provider,
image_url,
operation,
None,
@@ -77,11 +82,12 @@ async fn download_remote_image_with_deadline(
}
tokio::time::timeout_at(
tokio::time::Instant::from_std(request_deadline),
download_remote_image(http_client, image_url),
download_remote_image(http_client, image_url, provider),
)
.await
.map_err(|_| {
request_budget_exhausted_error(
provider,
image_url,
operation,
None,
@@ -97,6 +103,7 @@ pub(crate) async fn download_images_from_urls(
task_id: String,
image_urls: Vec<String>,
candidate_count: u32,
provider: ImageProvider,
request_deadline: Option<Instant>,
operation: &str,
) -> Result<GeneratedImages, PlatformImageError> {
@@ -109,6 +116,7 @@ pub(crate) async fn download_images_from_urls(
download_remote_image_with_deadline(
http_client,
image_url.as_str(),
provider,
request_deadline,
operation,
)
@@ -126,6 +134,7 @@ pub(crate) async fn download_images_from_urls(
pub(crate) async fn resolve_reference_images(
http_client: &reqwest::Client,
reference_images: &[String],
provider: ImageProvider,
failure_context: &str,
request_deadline: Option<Instant>,
) -> Result<Vec<ReferenceImage>, PlatformImageError> {
@@ -135,7 +144,7 @@ pub(crate) async fn resolve_reference_images(
.count();
if reference_count > VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES {
return Err(PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: format!(
"{failure_context}:参考图最多允许 {VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES} 张,当前提交 {reference_count} 张。"
),
@@ -147,7 +156,7 @@ pub(crate) async fn resolve_reference_images(
if source.is_empty() {
continue;
}
if let Some(reference_image) = parse_reference_image_data_url(source, index)? {
if let Some(reference_image) = parse_reference_image_data_url(source, index, provider)? {
resolved.push(reference_image);
continue;
}
@@ -155,6 +164,7 @@ pub(crate) async fn resolve_reference_images(
let downloaded = download_remote_image_with_deadline(
http_client,
source,
provider,
request_deadline,
failure_context,
)
@@ -171,14 +181,14 @@ pub(crate) async fn resolve_reference_images(
continue;
}
return Err(PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: format!("{failure_context}:参考图必须是图片 Data URL 或 HTTP(S) URL。"),
});
}
if resolved.is_empty() {
return Err(PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: format!("{failure_context}:图片编辑需要至少一张参考图。"),
});
}
@@ -189,25 +199,26 @@ pub(crate) async fn resolve_reference_images(
pub(crate) fn parse_reference_image_data_url(
source: &str,
index: usize,
provider: ImageProvider,
) -> Result<Option<ReferenceImage>, PlatformImageError> {
let Some(body) = source.strip_prefix("data:") else {
return Ok(None);
};
let Some((mime_type, data)) = body.split_once(";base64,") else {
return Err(PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: "参考图 Data URL 必须是 base64 图片。".to_string(),
});
};
if !mime_type.starts_with("image/") {
return Err(PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: "参考图 Data URL 必须是图片类型。".to_string(),
});
}
let bytes = BASE64_STANDARD.decode(data.trim()).map_err(|error| {
PlatformImageError::InvalidRequest {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message: format!("参考图 Data URL 解码失败:{error}"),
}
})?;
@@ -290,9 +301,13 @@ pub(crate) fn infer_image_mime_type(bytes: &[u8]) -> Option<String> {
None
}
fn map_simple_request_error(message: String, endpoint: Option<String>) -> PlatformImageError {
fn map_simple_request_error(
message: String,
endpoint: Option<String>,
provider: ImageProvider,
) -> PlatformImageError {
PlatformImageError::Request {
provider: VECTOR_ENGINE_PROVIDER,
provider: provider.as_str(),
message,
endpoint,
timeout: false,
@@ -349,6 +364,7 @@ mod tests {
let error = download_remote_image_with_deadline(
&http_client,
"http://127.0.0.1:9/not-called.png",
ImageProvider::VectorEngine,
Some(Instant::now()),
"下载测试图片失败",
)
@@ -387,6 +403,7 @@ mod tests {
let error = download_remote_image_with_deadline(
&reqwest::Client::new(),
format!("http://{address}/pending.png").as_str(),
ImageProvider::VectorEngine,
Some(Instant::now() + Duration::from_millis(100)),
"下载测试图片失败",
)
@@ -134,6 +134,7 @@ pub async fn create_raw_image_edit(
effective_request_timeout_ms(settings.request_timeout_ms, settings.request_deadline)
else {
return Err(request_budget_exhausted_error(
settings.provider,
url.as_str(),
failure_context,
Some(GPT_IMAGE_2_5_EDIT_MODEL),