fix(gateway): OpenAI 图像 finalize 统一返回 b64_json 并对齐 codex CLI 候选流式上游

- finalize 移除 response_format=url 的 data URL 分支,统一输出 b64_json
- image planner decision 依据请求体中的 stream 字段决定 upstream_is_stream
- openai chat sync plan 对 codex+openai:cli 候选强制上游流式
- 扩充 conversion registry 与 standard matrix 的全量 surface 对端测试
This commit is contained in:
fawney19
2026-04-24 19:01:11 +08:00
parent 4618184516
commit d0d71aa51a
6 changed files with 363 additions and 41 deletions
@@ -57,6 +57,11 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
extra_fields.insert("proxy".to_string(), proxy_value);
}
extra_fields.insert("image_request".to_string(), resolved.input_summary.clone());
let upstream_is_stream = resolved
.provider_request_body
.get("stream")
.and_then(serde_json::Value::as_bool)
.unwrap_or(spec_metadata.require_streaming);
let report_context = build_local_execution_report_context(LocalExecutionReportContextParts {
auth_context: &input.auth_context,
request_id: trace_id,
@@ -82,7 +87,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
original_request_body_json: Some(body_json),
original_request_body_base64: body_base64,
client_requested_stream: spec_metadata.require_streaming,
upstream_is_stream: spec_metadata.require_streaming,
upstream_is_stream,
has_envelope: false,
needs_conversion: false,
extra_fields,
@@ -117,7 +122,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat
proxy,
tls_profile,
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream: spec_metadata.require_streaming,
upstream_is_stream,
report_kind: spec_metadata.report_kind.map(ToOwned::to_owned),
report_context: Some(report_context),
auth_context: input.auth_context.clone(),
@@ -10,12 +10,21 @@ use super::diagnostic::{
set_local_openai_chat_candidate_evaluation_diagnostic, set_local_openai_chat_miss_diagnostic,
};
use super::resolve::resolve_local_openai_chat_decision_input;
use crate::ai_pipeline::planner::common::OPENAI_CHAT_SYNC_PLAN_KIND;
use crate::ai_pipeline::planner::common::{
force_upstream_streaming_for_provider, OPENAI_CHAT_SYNC_PLAN_KIND,
};
use crate::ai_pipeline::planner::plan_builders::{
build_openai_chat_sync_plan_from_decision, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::planner::runtime_miss::apply_local_runtime_candidate_terminal_reason;
fn openai_chat_sync_upstream_is_stream_for_candidate(
provider_type: &str,
provider_api_format: &str,
) -> bool {
force_upstream_streaming_for_provider(provider_type, provider_api_format)
}
pub(crate) async fn build_local_openai_chat_sync_plan_and_reports(
state: &AppState,
parts: &http::request::Parts,
@@ -88,6 +97,10 @@ pub(crate) async fn build_local_openai_chat_sync_plan_and_reports(
let mut plans = Vec::new();
for attempt in attempts {
let upstream_is_stream = openai_chat_sync_upstream_is_stream_for_candidate(
attempt.eligible.transport.provider.provider_type.as_str(),
attempt.eligible.provider_api_format.as_str(),
);
let Some(payload) = maybe_build_local_openai_chat_decision_payload_for_candidate(
state,
parts,
@@ -97,7 +110,7 @@ pub(crate) async fn build_local_openai_chat_sync_plan_and_reports(
attempt,
OPENAI_CHAT_SYNC_PLAN_KIND,
"openai_chat_sync_success",
false,
upstream_is_stream,
)
.await
else {
@@ -121,3 +134,24 @@ pub(crate) async fn build_local_openai_chat_sync_plan_and_reports(
Ok(plans)
}
#[cfg(test)]
mod tests {
use super::openai_chat_sync_upstream_is_stream_for_candidate;
#[test]
fn openai_chat_sync_forces_streaming_for_codex_openai_cli_candidates() {
assert!(openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
"openai:cli"
));
assert!(!openai_chat_sync_upstream_is_stream_for_candidate(
"openai",
"openai:cli"
));
assert!(!openai_chat_sync_upstream_is_stream_for_candidate(
"codex",
"openai:chat"
));
}
}