mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 实现 executor 流式执行路径及完善 gateway 多格式 plan 构建
- Rust executor 新增流式 plan 支持,覆盖 openai/claude/gemini 的 chat/cli/video 格式 - Gateway 新增 finalize-sync、report-stream 端点及 stream report 模型 - 新增 video sync 操作(create/cancel/remix/delete)的 plan 构建 - 修正 OpenAI Responses(openai:cli) SSE 格式: 添加 event: 行、移除 [DONE] 哨兵 - 统一 OpenAI CLI normalizer 的 ID 生成方法
This commit is contained in:
@@ -11,6 +11,7 @@ use base64::Engine as _;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
use tokio_util::io::StreamReader;
|
||||
use tracing::warn;
|
||||
@@ -28,6 +29,18 @@ const GEMINI_FILES_UPLOAD_PLAN_KIND: &str = "gemini_files_upload";
|
||||
const GEMINI_FILES_DELETE_PLAN_KIND: &str = "gemini_files_delete";
|
||||
const GEMINI_FILES_DOWNLOAD_PLAN_KIND: &str = "gemini_files_download";
|
||||
const OPENAI_VIDEO_CONTENT_PLAN_KIND: &str = "openai_video_content";
|
||||
const OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "openai_video_cancel_sync";
|
||||
const OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND: &str = "openai_video_remix_sync";
|
||||
const OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND: &str = "openai_video_delete_sync";
|
||||
const GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND: &str = "gemini_video_create_sync";
|
||||
const GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "gemini_video_cancel_sync";
|
||||
const OPENAI_CHAT_STREAM_PLAN_KIND: &str = "openai_chat_stream";
|
||||
const CLAUDE_CHAT_STREAM_PLAN_KIND: &str = "claude_chat_stream";
|
||||
const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream";
|
||||
const OPENAI_CLI_STREAM_PLAN_KIND: &str = "openai_cli_stream";
|
||||
const CLAUDE_CLI_STREAM_PLAN_KIND: &str = "claude_cli_stream";
|
||||
const GEMINI_CLI_STREAM_PLAN_KIND: &str = "gemini_cli_stream";
|
||||
const OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND: &str = "openai_video_create_sync";
|
||||
const OPENAI_CHAT_SYNC_PLAN_KIND: &str = "openai_chat_sync";
|
||||
const OPENAI_CLI_SYNC_PLAN_KIND: &str = "openai_cli_sync";
|
||||
const OPENAI_COMPACT_SYNC_PLAN_KIND: &str = "openai_compact_sync";
|
||||
@@ -81,6 +94,20 @@ struct GatewaySyncReportRequest {
|
||||
telemetry: Option<ExecutionTelemetry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GatewayStreamReportRequest {
|
||||
trace_id: String,
|
||||
report_kind: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
report_context: Option<serde_json::Value>,
|
||||
status_code: u16,
|
||||
headers: BTreeMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body_base64: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
telemetry: Option<ExecutionTelemetry>,
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_execute_via_executor_sync(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
@@ -205,6 +232,7 @@ pub(crate) async fn maybe_execute_via_executor_sync(
|
||||
pub(crate) async fn maybe_execute_via_executor_stream(
|
||||
state: &AppState,
|
||||
parts: &http::request::Parts,
|
||||
body_bytes: &Bytes,
|
||||
trace_id: &str,
|
||||
decision: Option<&GatewayControlDecision>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
@@ -222,14 +250,35 @@ pub(crate) async fn maybe_execute_via_executor_stream(
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let (body_json, body_base64) = if is_json_request(&parts.headers) {
|
||||
if body_bytes.is_empty() {
|
||||
(json!({}), None)
|
||||
} else {
|
||||
match serde_json::from_slice::<serde_json::Value>(body_bytes) {
|
||||
Ok(value) => (value, None),
|
||||
Err(_) => return Ok(None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(
|
||||
json!({}),
|
||||
(!body_bytes.is_empty())
|
||||
.then(|| base64::engine::general_purpose::STANDARD.encode(body_bytes)),
|
||||
)
|
||||
};
|
||||
|
||||
if !is_matching_stream_request(plan_kind, parts, &body_json) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let request_payload = GatewayControlPlanRequest {
|
||||
trace_id: trace_id.to_string(),
|
||||
method: parts.method.to_string(),
|
||||
path: parts.uri.path().to_string(),
|
||||
query_string: parts.uri.query().map(ToOwned::to_owned),
|
||||
headers: collect_control_headers(&parts.headers),
|
||||
body_json: json!({}),
|
||||
body_base64: None,
|
||||
body_json,
|
||||
body_base64,
|
||||
auth_context: decision.auth_context.clone(),
|
||||
};
|
||||
|
||||
@@ -295,11 +344,14 @@ pub(crate) async fn maybe_execute_via_executor_stream(
|
||||
|
||||
execute_executor_stream(
|
||||
state,
|
||||
control_base_url,
|
||||
executor_base_url,
|
||||
plan,
|
||||
trace_id,
|
||||
decision,
|
||||
plan_kind,
|
||||
payload.report_kind,
|
||||
payload.report_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -308,19 +360,69 @@ fn resolve_direct_executor_stream_plan_kind(
|
||||
parts: &http::request::Parts,
|
||||
decision: &GatewayControlDecision,
|
||||
) -> Option<&'static str> {
|
||||
if parts.method != http::Method::GET || decision.route_class.as_deref() != Some("ai_public") {
|
||||
if decision.route_class.as_deref() != Some("ai_public") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("gemini")
|
||||
&& decision.route_kind.as_deref() == Some("files")
|
||||
&& parts.method == http::Method::GET
|
||||
&& parts.uri.path().ends_with(":download")
|
||||
{
|
||||
return Some(GEMINI_FILES_DOWNLOAD_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("chat")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path() == "/v1/chat/completions"
|
||||
{
|
||||
return Some(OPENAI_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("claude")
|
||||
&& decision.route_kind.as_deref() == Some("chat")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path() == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("claude")
|
||||
&& decision.route_kind.as_deref() == Some("cli")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path() == "/v1/messages"
|
||||
{
|
||||
return Some(CLAUDE_CLI_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("gemini")
|
||||
&& decision.route_kind.as_deref() == Some("chat")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().ends_with(":streamGenerateContent")
|
||||
{
|
||||
return Some(GEMINI_CHAT_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("gemini")
|
||||
&& decision.route_kind.as_deref() == Some("cli")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().ends_with(":streamGenerateContent")
|
||||
{
|
||||
return Some(GEMINI_CLI_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("cli")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path() == "/v1/responses"
|
||||
{
|
||||
return Some(OPENAI_CLI_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::GET
|
||||
&& parts.uri.path().ends_with("/content")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CONTENT_PLAN_KIND);
|
||||
@@ -337,6 +439,56 @@ fn resolve_direct_executor_sync_plan_kind(
|
||||
return None;
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().starts_with("/v1/videos/")
|
||||
&& parts.uri.path().ends_with("/cancel")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().starts_with("/v1/videos/")
|
||||
&& parts.uri.path().ends_with("/remix")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path() == "/v1/videos"
|
||||
{
|
||||
return Some(OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::DELETE
|
||||
&& parts.uri.path().starts_with("/v1/videos/")
|
||||
{
|
||||
return Some(OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("gemini")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().ends_with(":cancel")
|
||||
{
|
||||
return Some(GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("gemini")
|
||||
&& decision.route_kind.as_deref() == Some("video")
|
||||
&& parts.method == http::Method::POST
|
||||
&& parts.uri.path().ends_with(":predictLongRunning")
|
||||
{
|
||||
return Some(GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND);
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("openai")
|
||||
&& decision.route_kind.as_deref() == Some("chat")
|
||||
&& parts.method == http::Method::POST
|
||||
@@ -419,13 +571,17 @@ fn resolve_direct_executor_sync_plan_kind(
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // internal function, grouping would add unnecessary indirection
|
||||
async fn execute_executor_stream(
|
||||
state: &AppState,
|
||||
control_base_url: &str,
|
||||
executor_base_url: &str,
|
||||
plan: ExecutionPlan,
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
plan_kind: &str,
|
||||
report_kind: Option<String>,
|
||||
report_context: Option<serde_json::Value>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let response = match state
|
||||
.client
|
||||
@@ -469,6 +625,10 @@ async fn execute_executor_stream(
|
||||
));
|
||||
};
|
||||
|
||||
if should_fallback_to_control_stream(plan_kind, status_code) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if status_code >= 400 {
|
||||
let error_body = collect_error_body(&mut lines).await?;
|
||||
return Ok(Some(build_executor_error_response(
|
||||
@@ -481,8 +641,18 @@ async fn execute_executor_stream(
|
||||
)?));
|
||||
}
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<Result<Bytes, IoError>>(16);
|
||||
let state_for_report = state.clone();
|
||||
let trace_id_owned = trace_id.to_string();
|
||||
let body_stream = stream! {
|
||||
let control_base_url_owned = control_base_url.to_string();
|
||||
let headers_for_report = headers.clone();
|
||||
let report_kind_owned = report_kind.clone();
|
||||
let report_context_owned = report_context.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut buffered_body = Vec::new();
|
||||
let mut telemetry: Option<ExecutionTelemetry> = None;
|
||||
let mut reached_eof = false;
|
||||
|
||||
loop {
|
||||
let next_frame = match read_next_frame(&mut lines).await {
|
||||
Ok(frame) => frame,
|
||||
@@ -496,20 +666,35 @@ async fn execute_executor_stream(
|
||||
};
|
||||
match frame.payload {
|
||||
StreamFramePayload::Data { chunk_b64, text } => {
|
||||
if let Some(chunk_b64) = chunk_b64 {
|
||||
let chunk = if let Some(chunk_b64) = chunk_b64 {
|
||||
match base64::engine::general_purpose::STANDARD.decode(chunk_b64) {
|
||||
Ok(decoded) => yield Ok::<Bytes, IoError>(Bytes::from(decoded)),
|
||||
Ok(decoded) => decoded,
|
||||
Err(err) => {
|
||||
warn!(trace_id = %trace_id_owned, error = %err, "gateway failed to decode executor chunk");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if let Some(text) = text {
|
||||
yield Ok::<Bytes, IoError>(Bytes::from(text.into_bytes()));
|
||||
text.into_bytes()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if chunk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
buffered_body.extend_from_slice(&chunk);
|
||||
let _ = tx.send(Ok(Bytes::from(chunk))).await;
|
||||
}
|
||||
StreamFramePayload::Telemetry {
|
||||
telemetry: frame_telemetry,
|
||||
} => {
|
||||
telemetry = Some(frame_telemetry);
|
||||
}
|
||||
StreamFramePayload::Eof { .. } => {
|
||||
reached_eof = true;
|
||||
break;
|
||||
}
|
||||
StreamFramePayload::Telemetry { .. } => {}
|
||||
StreamFramePayload::Eof { .. } => break,
|
||||
StreamFramePayload::Error { error } => {
|
||||
warn!(trace_id = %trace_id_owned, error = %error.message, "executor stream emitted error frame");
|
||||
break;
|
||||
@@ -517,6 +702,39 @@ async fn execute_executor_stream(
|
||||
StreamFramePayload::Headers { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
if reached_eof {
|
||||
if let Some(report_kind) = report_kind_owned {
|
||||
let report = GatewayStreamReportRequest {
|
||||
trace_id: trace_id_owned.clone(),
|
||||
report_kind,
|
||||
report_context: report_context_owned,
|
||||
status_code,
|
||||
headers: headers_for_report,
|
||||
body_base64: (!buffered_body.is_empty())
|
||||
.then(|| base64::engine::general_purpose::STANDARD.encode(&buffered_body)),
|
||||
telemetry,
|
||||
};
|
||||
if let Err(err) = submit_stream_report(
|
||||
&state_for_report,
|
||||
&control_base_url_owned,
|
||||
&trace_id_owned,
|
||||
report,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to submit stream execution report");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let body_stream = stream! {
|
||||
while let Some(item) = rx.recv().await {
|
||||
yield item;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(build_client_response_from_parts(
|
||||
@@ -574,6 +792,25 @@ async fn execute_executor_sync(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if should_finalize_sync_response(plan_kind) {
|
||||
let payload = GatewaySyncReportRequest {
|
||||
trace_id: trace_id.to_string(),
|
||||
report_kind: report_kind.unwrap_or_default(),
|
||||
report_context,
|
||||
status_code: result.status_code,
|
||||
headers: headers.clone(),
|
||||
body_json: body_json.clone(),
|
||||
body_base64: body_base64.clone(),
|
||||
telemetry: result.telemetry.clone(),
|
||||
};
|
||||
let response = submit_sync_finalize(state, control_base_url, trace_id, payload).await?;
|
||||
return Ok(Some(build_client_response(
|
||||
response,
|
||||
trace_id,
|
||||
Some(decision),
|
||||
)?));
|
||||
}
|
||||
|
||||
if let Some(report_kind) = report_kind {
|
||||
let report = GatewaySyncReportRequest {
|
||||
trace_id: trace_id.to_string(),
|
||||
@@ -604,9 +841,23 @@ fn should_fallback_to_control_sync(
|
||||
result: &ExecutionResult,
|
||||
body_json: Option<&serde_json::Value>,
|
||||
) -> bool {
|
||||
if matches!(
|
||||
plan_kind,
|
||||
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND | GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND
|
||||
) {
|
||||
return result.status_code >= 400;
|
||||
}
|
||||
|
||||
if plan_kind == OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND {
|
||||
return result.status_code >= 400 && result.status_code != 404;
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
plan_kind,
|
||||
OPENAI_CHAT_SYNC_PLAN_KIND
|
||||
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
|
||||
| GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| OPENAI_CHAT_SYNC_PLAN_KIND
|
||||
| OPENAI_CLI_SYNC_PLAN_KIND
|
||||
| OPENAI_COMPACT_SYNC_PLAN_KIND
|
||||
| CLAUDE_CHAT_SYNC_PLAN_KIND
|
||||
@@ -628,6 +879,50 @@ fn should_fallback_to_control_sync(
|
||||
body_json.get("error").is_some()
|
||||
}
|
||||
|
||||
fn should_finalize_sync_response(plan_kind: &str) -> bool {
|
||||
matches!(
|
||||
plan_kind,
|
||||
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
|
||||
| OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND
|
||||
| GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND
|
||||
| GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND
|
||||
)
|
||||
}
|
||||
|
||||
fn should_fallback_to_control_stream(plan_kind: &str, status_code: u16) -> bool {
|
||||
matches!(
|
||||
plan_kind,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| GEMINI_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_CLI_STREAM_PLAN_KIND
|
||||
| CLAUDE_CLI_STREAM_PLAN_KIND
|
||||
| GEMINI_CLI_STREAM_PLAN_KIND
|
||||
) && status_code >= 400
|
||||
}
|
||||
|
||||
fn is_matching_stream_request(
|
||||
plan_kind: &str,
|
||||
parts: &http::request::Parts,
|
||||
body_json: &serde_json::Value,
|
||||
) -> bool {
|
||||
match plan_kind {
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_CLI_STREAM_PLAN_KIND
|
||||
| CLAUDE_CLI_STREAM_PLAN_KIND => body_json
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND | GEMINI_CLI_STREAM_PLAN_KIND => {
|
||||
parts.uri.path().ends_with(":streamGenerateContent")
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
type DecodedBody = (Vec<u8>, Option<serde_json::Value>, Option<String>);
|
||||
|
||||
fn decode_execution_result_body(
|
||||
@@ -687,6 +982,56 @@ async fn submit_sync_report(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn submit_sync_finalize(
|
||||
state: &AppState,
|
||||
control_base_url: &str,
|
||||
trace_id: &str,
|
||||
payload: GatewaySyncReportRequest,
|
||||
) -> Result<reqwest::Response, GatewayError> {
|
||||
state
|
||||
.client
|
||||
.post(format!(
|
||||
"{control_base_url}/api/internal/gateway/finalize-sync"
|
||||
))
|
||||
.header(TRACE_ID_HEADER, trace_id)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| GatewayError::ControlUnavailable {
|
||||
trace_id: trace_id.to_string(),
|
||||
message: err.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn submit_stream_report(
|
||||
state: &AppState,
|
||||
control_base_url: &str,
|
||||
trace_id: &str,
|
||||
payload: GatewayStreamReportRequest,
|
||||
) -> Result<(), GatewayError> {
|
||||
let response = state
|
||||
.client
|
||||
.post(format!(
|
||||
"{control_base_url}/api/internal/gateway/report-stream"
|
||||
))
|
||||
.header(TRACE_ID_HEADER, trace_id)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| GatewayError::ControlUnavailable {
|
||||
trace_id: trace_id.to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
|
||||
response
|
||||
.error_for_status()
|
||||
.map_err(|err| GatewayError::ControlUnavailable {
|
||||
trace_id: trace_id.to_string(),
|
||||
message: err.to_string(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn collect_error_body<R>(
|
||||
lines: &mut FramedRead<R, LinesCodec>,
|
||||
) -> Result<Vec<u8>, GatewayError>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ use tracing::info;
|
||||
|
||||
use crate::gateway::constants::*;
|
||||
use crate::gateway::headers::{
|
||||
extract_or_generate_trace_id, header_value_str, should_skip_request_header,
|
||||
extract_or_generate_trace_id, header_value_str, is_json_request, should_skip_request_header,
|
||||
};
|
||||
use crate::gateway::{
|
||||
build_client_response, maybe_execute_via_control, maybe_execute_via_executor_stream,
|
||||
@@ -132,6 +132,20 @@ pub(crate) async fn proxy_request(
|
||||
let buffered_body = to_bytes(body, usize::MAX)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
let stream_request = request_wants_stream(&parts, &buffered_body);
|
||||
if stream_request {
|
||||
if let Some(executor_response) = maybe_execute_via_executor_stream(
|
||||
&state,
|
||||
&parts,
|
||||
&buffered_body,
|
||||
&trace_id,
|
||||
control_decision.as_ref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(executor_response);
|
||||
}
|
||||
}
|
||||
if let Some(executor_response) = maybe_execute_via_executor_sync(
|
||||
&state,
|
||||
&parts,
|
||||
@@ -143,11 +157,18 @@ pub(crate) async fn proxy_request(
|
||||
{
|
||||
return Ok(executor_response);
|
||||
}
|
||||
if let Some(executor_response) =
|
||||
maybe_execute_via_executor_stream(&state, &parts, &trace_id, control_decision.as_ref())
|
||||
.await?
|
||||
{
|
||||
return Ok(executor_response);
|
||||
if parts.method != http::Method::POST {
|
||||
if let Some(executor_response) = maybe_execute_via_executor_stream(
|
||||
&state,
|
||||
&parts,
|
||||
&buffered_body,
|
||||
&trace_id,
|
||||
control_decision.as_ref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(executor_response);
|
||||
}
|
||||
}
|
||||
if let Some(control_response) = maybe_execute_via_control(
|
||||
&state,
|
||||
@@ -202,3 +223,16 @@ pub(crate) async fn proxy_request(
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn request_wants_stream(parts: &http::request::Parts, body: &axum::body::Bytes) -> bool {
|
||||
if parts.uri.path().contains(":streamGenerateContent") {
|
||||
return true;
|
||||
}
|
||||
if !is_json_request(&parts.headers) || body.is_empty() {
|
||||
return false;
|
||||
}
|
||||
serde_json::from_slice::<serde_json::Value>(body)
|
||||
.ok()
|
||||
.and_then(|value| value.get("stream").and_then(|stream| stream.as_bool()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user