mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(image): 接入 ChatGPT Web 生图反代
This commit is contained in:
2317
apps/aether-gateway/src/execution_runtime/chatgpt_web_image.rs
Normal file
2317
apps/aether-gateway/src/execution_runtime/chatgpt_web_image.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -963,4 +963,53 @@ mod tests {
|
||||
.await
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn chatgpt_web_report_context_stops_local_sync_failover_on_transport_errors() {
|
||||
let result = ExecutionResult {
|
||||
request_id: "req-1".to_string(),
|
||||
candidate_id: None,
|
||||
status_code: 503,
|
||||
headers: Default::default(),
|
||||
body: None,
|
||||
telemetry: None,
|
||||
error: None,
|
||||
};
|
||||
let local_report_context = serde_json::json!({
|
||||
"chatgpt_web_image": true,
|
||||
"candidate_index": 0,
|
||||
"retry_index": 0,
|
||||
"local_failover_policy": {
|
||||
"stop_status_codes": [400, 401, 403, 429, 500, 502, 503, 504],
|
||||
"error_stop_patterns": [
|
||||
{"pattern": ".*"}
|
||||
]
|
||||
}
|
||||
});
|
||||
let state = build_state_with_provider_config(None);
|
||||
let plan = sample_plan();
|
||||
|
||||
assert!(
|
||||
should_stop_local_candidate_failover_sync(
|
||||
&state,
|
||||
&plan,
|
||||
"openai_image_sync",
|
||||
Some(&local_report_context),
|
||||
&result,
|
||||
Some("{\"error\":{\"code\":\"chatgpt_web_image_execution_unavailable\"}}"),
|
||||
)
|
||||
.await
|
||||
);
|
||||
assert!(
|
||||
!should_retry_next_local_candidate_sync(
|
||||
&state,
|
||||
&plan,
|
||||
"openai_image_sync",
|
||||
Some(&local_report_context),
|
||||
&result,
|
||||
Some("{\"error\":{\"code\":\"chatgpt_web_image_execution_unavailable\"}}"),
|
||||
)
|
||||
.await
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
mod chatgpt_web_image;
|
||||
mod constants;
|
||||
mod fallback;
|
||||
mod kiro_web_search;
|
||||
|
||||
@@ -51,6 +51,7 @@ use crate::clock::current_unix_ms as current_request_candidate_unix_ms;
|
||||
use crate::constants::{CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::build_direct_execution_frame_stream;
|
||||
use crate::execution_runtime::chatgpt_web_image::maybe_execute_chatgpt_web_image_stream;
|
||||
use crate::execution_runtime::kiro_web_search::maybe_execute_kiro_web_search_stream;
|
||||
use crate::execution_runtime::oauth_retry::refresh_oauth_plan_auth_for_retry;
|
||||
#[cfg(test)]
|
||||
@@ -447,6 +448,57 @@ pub(crate) async fn execute_execution_runtime_stream(
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
match maybe_execute_chatgpt_web_image_stream(state, &plan, report_context.as_ref()).await {
|
||||
Ok(Some(chatgpt_web_image)) => {
|
||||
return execute_stream_from_frame_stream(
|
||||
state,
|
||||
plan,
|
||||
trace_id,
|
||||
decision,
|
||||
plan_kind,
|
||||
report_kind,
|
||||
chatgpt_web_image.report_context.or(report_context),
|
||||
candidate_started_unix_secs,
|
||||
stream_started_at,
|
||||
chatgpt_web_image.frame_stream,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
event_name = "chatgpt_web_image_execution_unavailable",
|
||||
log_type = "ops",
|
||||
trace_id = %trace_id,
|
||||
request_id = %plan_request_id_for_log,
|
||||
candidate_id = ?plan.candidate_id,
|
||||
provider_name = provider_name.as_str(),
|
||||
endpoint_id = %endpoint_id,
|
||||
key_id = %key_id,
|
||||
model_name = model_name.as_str(),
|
||||
candidate_index = candidate_index.as_str(),
|
||||
error = %err,
|
||||
"gateway ChatGPT-Web image stream execution unavailable"
|
||||
);
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
state,
|
||||
&plan,
|
||||
report_context.as_ref(),
|
||||
SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: None,
|
||||
error_type: Some("chatgpt_web_image_execution_unavailable".to_string()),
|
||||
error_message: Some(format!("{err:?}")),
|
||||
latency_ms: None,
|
||||
started_at_unix_ms: Some(candidate_started_unix_secs),
|
||||
finished_at_unix_ms: Some(terminal_unix_secs),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let execution = match execute_in_process_stream_with_oauth_retry(
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::api::response::{
|
||||
};
|
||||
use crate::clock::current_unix_ms as current_request_candidate_unix_ms;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::execution_runtime::chatgpt_web_image::maybe_execute_chatgpt_web_image_sync;
|
||||
use crate::execution_runtime::oauth_retry::refresh_oauth_plan_auth_for_retry;
|
||||
#[cfg(test)]
|
||||
use crate::execution_runtime::remote_compat::post_sync_plan_to_remote_execution_runtime;
|
||||
@@ -193,11 +194,47 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
.await;
|
||||
#[cfg(not(test))]
|
||||
let mut result = {
|
||||
match DirectSyncExecutionRuntime::new().execute_sync(&plan).await {
|
||||
Ok(result) => result,
|
||||
match maybe_execute_chatgpt_web_image_sync(state, &plan, report_context.as_ref()).await {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => match DirectSyncExecutionRuntime::new().execute_sync(&plan).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "sync_execution_runtime_unavailable",
|
||||
log_type = "ops",
|
||||
trace_id = %trace_id,
|
||||
request_id = %plan_request_id_for_log,
|
||||
candidate_id = ?plan_candidate_id,
|
||||
provider_name,
|
||||
endpoint_id,
|
||||
key_id,
|
||||
model_name,
|
||||
candidate_index = candidate_index.as_str(),
|
||||
error = %err,
|
||||
"gateway in-process sync execution unavailable"
|
||||
);
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
state,
|
||||
&plan,
|
||||
report_context.as_ref(),
|
||||
SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: None,
|
||||
error_type: Some("execution_runtime_unavailable".to_string()),
|
||||
error_message: Some(err.to_string()),
|
||||
latency_ms: None,
|
||||
started_at_unix_ms: Some(candidate_started_unix_secs),
|
||||
finished_at_unix_ms: Some(terminal_unix_secs),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "sync_execution_runtime_unavailable",
|
||||
event_name = "chatgpt_web_image_execution_unavailable",
|
||||
log_type = "ops",
|
||||
trace_id = %trace_id,
|
||||
request_id = %plan_request_id_for_log,
|
||||
@@ -208,7 +245,7 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
model_name,
|
||||
candidate_index = candidate_index.as_str(),
|
||||
error = %err,
|
||||
"gateway in-process sync execution unavailable"
|
||||
"gateway ChatGPT-Web image execution unavailable"
|
||||
);
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
@@ -218,7 +255,7 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: None,
|
||||
error_type: Some("execution_runtime_unavailable".to_string()),
|
||||
error_type: Some("chatgpt_web_image_execution_unavailable".to_string()),
|
||||
error_message: Some(err.to_string()),
|
||||
latency_ms: None,
|
||||
started_at_unix_ms: Some(candidate_started_unix_secs),
|
||||
@@ -275,11 +312,48 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
match DirectSyncExecutionRuntime::new().execute_sync(&plan).await {
|
||||
Ok(result) => result,
|
||||
match maybe_execute_chatgpt_web_image_sync(state, &plan, report_context.as_ref()).await
|
||||
{
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => match DirectSyncExecutionRuntime::new().execute_sync(&plan).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "sync_execution_runtime_unavailable",
|
||||
log_type = "ops",
|
||||
trace_id = %trace_id,
|
||||
request_id = %plan_request_id_for_log,
|
||||
candidate_id = ?plan_candidate_id,
|
||||
provider_name,
|
||||
endpoint_id,
|
||||
key_id,
|
||||
model_name,
|
||||
candidate_index = candidate_index.as_str(),
|
||||
error = %err,
|
||||
"gateway in-process sync execution unavailable"
|
||||
);
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
state,
|
||||
&plan,
|
||||
report_context.as_ref(),
|
||||
SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: None,
|
||||
error_type: Some("execution_runtime_unavailable".to_string()),
|
||||
error_message: Some(err.to_string()),
|
||||
latency_ms: None,
|
||||
started_at_unix_ms: Some(candidate_started_unix_secs),
|
||||
finished_at_unix_ms: Some(terminal_unix_secs),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "sync_execution_runtime_unavailable",
|
||||
event_name = "chatgpt_web_image_execution_unavailable",
|
||||
log_type = "ops",
|
||||
trace_id = %trace_id,
|
||||
request_id = %plan_request_id_for_log,
|
||||
@@ -290,7 +364,7 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
model_name,
|
||||
candidate_index = candidate_index.as_str(),
|
||||
error = %err,
|
||||
"gateway in-process sync execution unavailable"
|
||||
"gateway ChatGPT-Web image execution unavailable"
|
||||
);
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
@@ -300,7 +374,7 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: None,
|
||||
error_type: Some("execution_runtime_unavailable".to_string()),
|
||||
error_type: Some("chatgpt_web_image_execution_unavailable".to_string()),
|
||||
error_message: Some(err.to_string()),
|
||||
latency_ms: None,
|
||||
started_at_unix_ms: Some(candidate_started_unix_secs),
|
||||
|
||||
@@ -6,7 +6,8 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use aether_contracts::{
|
||||
ExecutionPlan, ExecutionResult, ExecutionTelemetry, ProxySnapshot, ResolvedTransportProfile,
|
||||
ResponseBody, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
|
||||
ResponseBody, EXECUTION_REQUEST_ACCEPT_INVALID_CERTS_HEADER,
|
||||
EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER,
|
||||
TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_HTTP1_ONLY,
|
||||
};
|
||||
use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation;
|
||||
@@ -141,6 +142,7 @@ pub(crate) struct DirectSyncExecutionRuntime;
|
||||
struct ExecutionTransportControls {
|
||||
follow_redirects: Option<bool>,
|
||||
http1_only: bool,
|
||||
accept_invalid_certs: bool,
|
||||
}
|
||||
|
||||
pub(crate) enum DirectUpstreamResponse {
|
||||
@@ -894,6 +896,9 @@ fn build_client(
|
||||
builder,
|
||||
transport_profile.map(|profile| profile.profile_id.as_str()),
|
||||
);
|
||||
if transport_controls.accept_invalid_certs {
|
||||
builder = builder.danger_accept_invalid_certs(true);
|
||||
}
|
||||
if let Some(proxy_url) = resolve_proxy_url(proxy)? {
|
||||
let proxy = reqwest::Proxy::all(&proxy_url)
|
||||
.map_err(ExecutionRuntimeTransportError::InvalidProxy)?;
|
||||
@@ -1025,6 +1030,7 @@ fn build_request_headers(
|
||||
|| normalized_key == "content-encoding"
|
||||
|| normalized_key == EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER
|
||||
|| normalized_key == EXECUTION_REQUEST_HTTP1_ONLY_HEADER
|
||||
|| normalized_key == EXECUTION_REQUEST_ACCEPT_INVALID_CERTS_HEADER
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1058,6 +1064,12 @@ fn resolve_execution_transport_controls(
|
||||
http1_only: execution_transport_header_value(headers, EXECUTION_REQUEST_HTTP1_ONLY_HEADER)
|
||||
.and_then(|value| parse_execution_transport_bool(value))
|
||||
.unwrap_or(false),
|
||||
accept_invalid_certs: execution_transport_header_value(
|
||||
headers,
|
||||
EXECUTION_REQUEST_ACCEPT_INVALID_CERTS_HEADER,
|
||||
)
|
||||
.and_then(|value| parse_execution_transport_bool(value))
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1201,9 +1213,10 @@ mod tests {
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::{
|
||||
build_client, execute_sync_plan, record_manual_proxy_request_failure,
|
||||
record_manual_proxy_request_outcome, record_manual_proxy_request_success,
|
||||
record_manual_proxy_stream_error, DirectSyncExecutionRuntime,
|
||||
build_client, build_request_headers, execute_sync_plan,
|
||||
record_manual_proxy_request_failure, record_manual_proxy_request_outcome,
|
||||
record_manual_proxy_request_success, record_manual_proxy_stream_error,
|
||||
resolve_execution_transport_controls, DirectSyncExecutionRuntime,
|
||||
ExecutionRuntimeTransportError, ExecutionTransportControls,
|
||||
};
|
||||
use crate::constants::{
|
||||
@@ -1285,6 +1298,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_sync_execution_runtime_strips_accept_invalid_certs_control_header() {
|
||||
let headers = BTreeMap::from([
|
||||
("content-type".into(), "application/json".into()),
|
||||
(
|
||||
"x-aether-execution-accept-invalid-certs".into(),
|
||||
"true".into(),
|
||||
),
|
||||
]);
|
||||
|
||||
let controls = resolve_execution_transport_controls(&headers);
|
||||
assert!(controls.accept_invalid_certs);
|
||||
|
||||
let forwarded = build_request_headers(&headers, None, false)
|
||||
.expect("headers should build after stripping internal controls");
|
||||
assert!(forwarded.get("content-type").is_some());
|
||||
assert!(forwarded
|
||||
.get("x-aether-execution-accept-invalid-certs")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
fn tunnel_proxy_snapshot(base_url: String) -> ProxySnapshot {
|
||||
ProxySnapshot {
|
||||
enabled: Some(true),
|
||||
|
||||
Reference in New Issue
Block a user