mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
Merge remote-tracking branch 'origin/pr-483' into merge-pr-483
# Conflicts: # apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs # apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/request.rs # apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs # apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/request.rs # apps/aether-gateway/src/execution_runtime/chatgpt_web_image.rs
This commit is contained in:
@@ -3,20 +3,22 @@ use std::io::Error as IoError;
|
||||
use std::time::Instant;
|
||||
|
||||
use aether_contracts::{
|
||||
ExecutionPlan, ExecutionResult, ExecutionTelemetry, RequestBody, ResolvedTransportProfile,
|
||||
ResponseBody, StreamFrame, StreamFramePayload, StreamFrameType,
|
||||
EXECUTION_REQUEST_ACCEPT_INVALID_CERTS_HEADER, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER,
|
||||
TRANSPORT_BACKEND_BROWSER_WREQ, TRANSPORT_HTTP_MODE_AUTO, TRANSPORT_POOL_SCOPE_KEY,
|
||||
ExecutionPlan, ExecutionResult, ExecutionStreamTerminalSummary, ExecutionTelemetry,
|
||||
RequestBody, ResolvedTransportProfile, ResponseBody, StreamFrame, StreamFramePayload,
|
||||
StreamFrameType, EXECUTION_REQUEST_ACCEPT_INVALID_CERTS_HEADER,
|
||||
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, TRANSPORT_BACKEND_BROWSER_WREQ,
|
||||
TRANSPORT_HTTP_MODE_AUTO, TRANSPORT_POOL_SCOPE_KEY,
|
||||
};
|
||||
use axum::body::Bytes;
|
||||
use base64::Engine as _;
|
||||
use chrono::{FixedOffset, Utc};
|
||||
use futures_util::stream::{self, BoxStream};
|
||||
use futures_util::StreamExt;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{json, Map, Value};
|
||||
use tracing::debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ai_serving::api::StreamingStandardTerminalObserver;
|
||||
use crate::clock::current_unix_secs;
|
||||
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
|
||||
use crate::execution_runtime::transport::{
|
||||
@@ -111,16 +113,15 @@ pub(crate) async fn maybe_execute_chatgpt_web_image_stream(
|
||||
Err(err) => chatgpt_web_transport_error_execution_result(plan, started_at, &err),
|
||||
};
|
||||
Ok(Some(ChatGptWebImageStream {
|
||||
frame_stream: execution_result_frame_stream(&result),
|
||||
frame_stream: execution_result_frame_stream(plan, &result, report_context),
|
||||
report_context: report_context.cloned(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_chatgpt_web_image_plan(plan: &ExecutionPlan, report_context: Option<&Value>) -> bool {
|
||||
if !plan.client_api_format.eq_ignore_ascii_case("openai:image")
|
||||
|| !plan
|
||||
.provider_api_format
|
||||
.eq_ignore_ascii_case("openai:image")
|
||||
if !plan
|
||||
.provider_api_format
|
||||
.eq_ignore_ascii_case("openai:image")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1510,9 +1511,12 @@ fn bytes_execution_result(
|
||||
}
|
||||
|
||||
fn execution_result_frame_stream(
|
||||
plan: &ExecutionPlan,
|
||||
result: &ExecutionResult,
|
||||
report_context: Option<&Value>,
|
||||
) -> BoxStream<'static, Result<Bytes, IoError>> {
|
||||
let body = execution_result_body_bytes_lossy(result);
|
||||
let terminal_summary = chatgpt_web_stream_terminal_summary(plan, result, report_context, &body);
|
||||
let mut frames = vec![
|
||||
StreamFrame {
|
||||
frame_type: StreamFrameType::Headers,
|
||||
@@ -1551,7 +1555,7 @@ fn execution_result_frame_stream(
|
||||
}),
|
||||
},
|
||||
});
|
||||
frames.push(StreamFrame::eof());
|
||||
frames.push(StreamFrame::eof_with_summary(terminal_summary));
|
||||
stream::iter(
|
||||
frames
|
||||
.into_iter()
|
||||
@@ -1560,6 +1564,81 @@ fn execution_result_frame_stream(
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn chatgpt_web_stream_terminal_summary(
|
||||
plan: &ExecutionPlan,
|
||||
result: &ExecutionResult,
|
||||
report_context: Option<&Value>,
|
||||
body: &[u8],
|
||||
) -> Option<ExecutionStreamTerminalSummary> {
|
||||
if !(200..300).contains(&result.status_code) || body.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let observer_context = chatgpt_web_stream_observer_context(plan, report_context);
|
||||
let mut observer = StreamingStandardTerminalObserver::default();
|
||||
let mut line_start = 0usize;
|
||||
for (index, byte) in body.iter().enumerate() {
|
||||
if *byte != b'\n' {
|
||||
continue;
|
||||
}
|
||||
observer
|
||||
.push_line(&observer_context, body[line_start..=index].to_vec())
|
||||
.ok()?;
|
||||
line_start = index.saturating_add(1);
|
||||
}
|
||||
if line_start < body.len() {
|
||||
observer
|
||||
.push_line(&observer_context, body[line_start..].to_vec())
|
||||
.ok()?;
|
||||
}
|
||||
observer.finish(&observer_context).ok().flatten()
|
||||
}
|
||||
|
||||
fn chatgpt_web_stream_observer_context(
|
||||
plan: &ExecutionPlan,
|
||||
report_context: Option<&Value>,
|
||||
) -> Value {
|
||||
let mut context = report_context
|
||||
.cloned()
|
||||
.filter(Value::is_object)
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let object = context
|
||||
.as_object_mut()
|
||||
.expect("observer context should be an object");
|
||||
object
|
||||
.entry("provider_api_format".to_string())
|
||||
.or_insert_with(|| Value::String(plan.provider_api_format.clone()));
|
||||
object
|
||||
.entry("client_api_format".to_string())
|
||||
.or_insert_with(|| Value::String(plan.client_api_format.clone()));
|
||||
object
|
||||
.entry("model".to_string())
|
||||
.or_insert_with(|| Value::String(plan.model_name.clone().unwrap_or_default()));
|
||||
if !object.contains_key("image_request") {
|
||||
if let Some(image_request) = chatgpt_web_image_request_context(plan) {
|
||||
object.insert("image_request".to_string(), image_request);
|
||||
}
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
fn chatgpt_web_image_request_context(plan: &ExecutionPlan) -> Option<Value> {
|
||||
let body = plan.body.json_body.as_ref()?.as_object()?;
|
||||
let mut image_request = Map::new();
|
||||
image_request.insert(
|
||||
"operation".to_string(),
|
||||
Value::String("generate".to_string()),
|
||||
);
|
||||
for key in ["model", "size", "quality", "output_format"] {
|
||||
if let Some(value) = body.get(key).and_then(Value::as_str).map(str::trim) {
|
||||
if !value.is_empty() {
|
||||
image_request.insert(key.to_string(), Value::String(value.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Value::Object(image_request))
|
||||
}
|
||||
|
||||
fn telemetry(started_at: Instant, upstream_bytes: u64) -> ExecutionTelemetry {
|
||||
let elapsed_ms = started_at.elapsed().as_millis() as u64;
|
||||
ExecutionTelemetry {
|
||||
@@ -2280,6 +2359,31 @@ data: [DONE]
|
||||
assert!(decoded_data.contains("\"width\":2"));
|
||||
assert!(decoded_data.contains("\"height\":3"));
|
||||
assert!(text.contains("\"type\":\"eof\""));
|
||||
let eof_frame = text
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
|
||||
.find(|frame| frame.get("type").and_then(Value::as_str) == Some("eof"))
|
||||
.expect("eof frame should exist");
|
||||
assert_eq!(
|
||||
eof_frame
|
||||
.get("payload")
|
||||
.and_then(|payload| payload.get("summary"))
|
||||
.and_then(|summary| summary.get("standardized_usage"))
|
||||
.and_then(|usage| usage.get("dimensions"))
|
||||
.and_then(|dimensions| dimensions.get("image_count"))
|
||||
.and_then(Value::as_u64),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
eof_frame
|
||||
.get("payload")
|
||||
.and_then(|payload| payload.get("summary"))
|
||||
.and_then(|summary| summary.get("standardized_usage"))
|
||||
.and_then(|usage| usage.get("dimensions"))
|
||||
.and_then(|dimensions| dimensions.get("image_size"))
|
||||
.and_then(Value::as_str),
|
||||
Some("1024x1024")
|
||||
);
|
||||
|
||||
handle.abort();
|
||||
}
|
||||
@@ -2314,6 +2418,32 @@ data: [DONE]
|
||||
assert_eq!(body["error"]["code"], "chatgpt_web_image_unsupported");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chatgpt_web_image_executor_accepts_marked_responses_client_plan() {
|
||||
let state = crate::AppState::new().expect("state should build");
|
||||
let mut plan = sample_plan(
|
||||
CHATGPT_WEB_DEFAULT_BASE_URL,
|
||||
json!({
|
||||
"error": {
|
||||
"message": "ChatGPT-Web 不支持该分辨率",
|
||||
"type": "invalid_request_error",
|
||||
"code": "chatgpt_web_image_unsupported"
|
||||
}
|
||||
}),
|
||||
false,
|
||||
);
|
||||
plan.client_api_format = "openai:responses".to_string();
|
||||
|
||||
let result = maybe_execute_chatgpt_web_image_sync(&state, &plan, None)
|
||||
.await
|
||||
.expect("executor should run")
|
||||
.expect("marked image provider plan should be intercepted");
|
||||
|
||||
assert_eq!(result.status_code, 400);
|
||||
let body = execution_result_json(&result).expect("error should be json");
|
||||
assert_eq!(body["error"]["code"], "chatgpt_web_image_unsupported");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chatgpt_web_image_stream_path_wraps_executor_result_as_ndjson_frames() {
|
||||
let state = crate::AppState::new().expect("state should build");
|
||||
|
||||
Reference in New Issue
Block a user