refactor ai serving modules and crates

This commit is contained in:
fawney19
2026-05-02 13:23:54 +08:00
parent 4fc7cecf30
commit c130d0e2c9
309 changed files with 21549 additions and 14265 deletions

View File

@@ -8,7 +8,8 @@ description = "Rust ingress gateway for Aether phase 3a transparent proxy"
[dependencies]
aether-admin.workspace = true
aether-ai-pipeline.workspace = true
aether-ai-serving.workspace = true
aether-ai-surfaces.workspace = true
aether-billing.workspace = true
aether-cache.workspace = true
aether-contracts.workspace = true

View File

@@ -1,71 +0,0 @@
use std::collections::BTreeMap;
const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
const MAX_BUFFER_SIZE: usize = MAX_MESSAGE_SIZE;
const MAX_ERRORS: usize = 5;
#[derive(Default)]
pub(crate) struct KiroToClaudeCliStreamState {
decoder: EventStreamDecoder,
state: KiroClaudeStreamState,
started: bool,
}
#[derive(Default)]
struct KiroClaudeStreamState {
model: String,
thinking_enabled: bool,
estimated_input_tokens: usize,
message_id: String,
output_tokens: usize,
context_input_tokens: Option<usize>,
next_block_index: usize,
open_blocks: BTreeMap<usize, String>,
text_block_index: Option<usize>,
thinking_block_index: Option<usize>,
tool_block_indices: BTreeMap<String, usize>,
thinking_buffer: String,
in_thinking_block: bool,
thinking_extracted: bool,
strip_thinking_leading_newline: bool,
has_tool_use: bool,
stop_reason_override: Option<String>,
had_error: bool,
last_content: String,
}
#[derive(Default)]
struct EventStreamDecoder {
buffer: Vec<u8>,
error_count: usize,
stopped: bool,
}
#[derive(Default)]
struct AwsHeaders {
values: BTreeMap<String, AwsHeaderValue>,
}
enum AwsHeaderValue {
Ignored,
String(String),
}
struct AwsEventFrame {
headers: AwsHeaders,
payload: Vec<u8>,
}
enum FrameParseError {
Incomplete,
Invalid(String),
}
#[path = "stream/decoder.rs"]
mod decoder;
#[path = "stream/state.rs"]
mod state;
#[cfg(test)]
#[path = "stream/tests.rs"]
mod tests;

View File

@@ -1,222 +0,0 @@
use std::collections::BTreeMap;
use super::{
AwsEventFrame, AwsHeaderValue, AwsHeaders, EventStreamDecoder, FrameParseError,
MAX_BUFFER_SIZE, MAX_ERRORS, MAX_MESSAGE_SIZE,
};
use crate::ai_pipeline::kiro_crc32 as crc32;
impl EventStreamDecoder {
pub(super) fn feed(&mut self, data: &[u8]) -> Result<(), String> {
if self.stopped || data.is_empty() {
return Ok(());
}
let new_size = self.buffer.len() + data.len();
if new_size > MAX_BUFFER_SIZE {
self.stopped = true;
return Err(format!(
"buffer overflow: size={new_size} max={MAX_BUFFER_SIZE}"
));
}
self.buffer.extend_from_slice(data);
Ok(())
}
pub(super) fn decode_available(&mut self) -> Result<Vec<AwsEventFrame>, String> {
let mut out = Vec::new();
if self.stopped {
return Ok(out);
}
loop {
match parse_frame(&self.buffer) {
Ok(Some((frame, consumed))) => {
if consumed == 0 {
break;
}
out.push(frame);
self.buffer.drain(..consumed);
self.error_count = 0;
}
Ok(None) => break,
Err(FrameParseError::Incomplete) => break,
Err(FrameParseError::Invalid(message)) => {
self.error_count += 1;
if self.error_count >= MAX_ERRORS {
self.stopped = true;
return Err(message);
}
if self.buffer.is_empty() {
break;
}
self.buffer.drain(..1);
}
}
}
Ok(out)
}
}
impl AwsHeaders {
fn get_string(&self, name: &str) -> Option<&str> {
match self.values.get(name) {
Some(AwsHeaderValue::String(value)) => Some(value.as_str()),
_ => None,
}
}
pub(super) fn message_type(&self) -> Option<&str> {
self.get_string(":message-type")
}
pub(super) fn event_type(&self) -> Option<&str> {
self.get_string(":event-type")
}
pub(super) fn exception_type(&self) -> Option<&str> {
self.get_string(":exception-type")
}
pub(super) fn error_code(&self) -> Option<&str> {
self.get_string(":error-code")
}
}
fn parse_frame(buffer: &[u8]) -> Result<Option<(AwsEventFrame, usize)>, FrameParseError> {
if buffer.len() < 12 {
return Ok(None);
}
let total_length = u32::from_be_bytes(buffer[0..4].try_into().expect("slice size")) as usize;
let header_length = u32::from_be_bytes(buffer[4..8].try_into().expect("slice size")) as usize;
let prelude_crc = u32::from_be_bytes(buffer[8..12].try_into().expect("slice size"));
if total_length < 16 {
return Err(FrameParseError::Invalid(format!(
"message too small: length={total_length}"
)));
}
if total_length > MAX_MESSAGE_SIZE {
return Err(FrameParseError::Invalid(format!(
"message too large: length={total_length}"
)));
}
if buffer.len() < total_length {
return Ok(None);
}
if crc32(&buffer[0..8]) != prelude_crc {
return Err(FrameParseError::Invalid("prelude crc mismatch".to_string()));
}
let message_crc = u32::from_be_bytes(
buffer[total_length - 4..total_length]
.try_into()
.expect("slice size"),
);
if crc32(&buffer[..total_length - 4]) != message_crc {
return Err(FrameParseError::Invalid("message crc mismatch".to_string()));
}
let headers_start = 12;
let headers_end = headers_start + header_length;
if headers_end > total_length - 4 {
return Err(FrameParseError::Invalid(
"header length exceeds frame boundary".to_string(),
));
}
let headers = parse_headers(&buffer[headers_start..headers_end], header_length)?;
let payload = buffer[headers_end..total_length - 4].to_vec();
Ok(Some((AwsEventFrame { headers, payload }, total_length)))
}
fn parse_headers(data: &[u8], header_length: usize) -> Result<AwsHeaders, FrameParseError> {
if data.len() < header_length {
return Err(FrameParseError::Incomplete);
}
let mut values = BTreeMap::new();
let mut offset = 0usize;
while offset < header_length {
ensure_header_bytes(data, offset, 1)?;
let name_len = data[offset] as usize;
offset += 1;
if name_len == 0 {
return Err(FrameParseError::Invalid(
"header name length cannot be 0".to_string(),
));
}
ensure_header_bytes(data, offset, name_len)?;
let name = String::from_utf8_lossy(&data[offset..offset + name_len]).to_string();
offset += name_len;
ensure_header_bytes(data, offset, 1)?;
let value_type = data[offset];
offset += 1;
let value = match value_type {
0 => AwsHeaderValue::Ignored,
1 => AwsHeaderValue::Ignored,
2 => {
ensure_header_bytes(data, offset, 1)?;
let _ = i8::from_be_bytes([data[offset]]);
offset += 1;
AwsHeaderValue::Ignored
}
3 => {
ensure_header_bytes(data, offset, 2)?;
let _ = i16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"));
offset += 2;
AwsHeaderValue::Ignored
}
4 => {
ensure_header_bytes(data, offset, 4)?;
let _ = i32::from_be_bytes(data[offset..offset + 4].try_into().expect("slice"));
offset += 4;
AwsHeaderValue::Ignored
}
5 | 8 => {
ensure_header_bytes(data, offset, 8)?;
let _ = i64::from_be_bytes(data[offset..offset + 8].try_into().expect("slice"));
offset += 8;
AwsHeaderValue::Ignored
}
6 => {
ensure_header_bytes(data, offset, 2)?;
let length = u16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"))
as usize;
offset += 2;
ensure_header_bytes(data, offset, length)?;
offset += length;
AwsHeaderValue::Ignored
}
7 => {
ensure_header_bytes(data, offset, 2)?;
let length = u16::from_be_bytes(data[offset..offset + 2].try_into().expect("slice"))
as usize;
offset += 2;
ensure_header_bytes(data, offset, length)?;
let out = String::from_utf8_lossy(&data[offset..offset + length]).to_string();
offset += length;
AwsHeaderValue::String(out)
}
9 => {
ensure_header_bytes(data, offset, 16)?;
offset += 16;
AwsHeaderValue::Ignored
}
other => {
return Err(FrameParseError::Invalid(format!(
"invalid header type: {other}"
)));
}
};
values.insert(name, value);
}
Ok(AwsHeaders { values })
}
fn ensure_header_bytes(data: &[u8], offset: usize, needed: usize) -> Result<(), FrameParseError> {
let available = data.len().saturating_sub(offset);
if available < needed {
return Err(FrameParseError::Incomplete);
}
Ok(())
}

View File

@@ -1,8 +0,0 @@
#[path = "state/blocks.rs"]
mod blocks;
#[path = "state/events.rs"]
mod events;
#[path = "state/finalize.rs"]
mod finalize;
#[path = "state/lifecycle.rs"]
mod lifecycle;

View File

@@ -1,97 +0,0 @@
use serde_json::{json, Value};
use super::super::KiroClaudeStreamState;
impl KiroClaudeStreamState {
pub(super) fn ensure_text_block_open(&mut self) -> Vec<Value> {
if let Some(idx) = self.text_block_index {
if self
.open_blocks
.get(&idx)
.map(|value| value == "text")
.unwrap_or(false)
{
return Vec::new();
}
}
let idx = self.next_block_index;
self.next_block_index += 1;
self.text_block_index = Some(idx);
self.open_blocks.insert(idx, "text".to_string());
vec![json!({
"type": "content_block_start",
"index": idx,
"content_block": {"type": "text", "text": ""}
})]
}
pub(super) fn ensure_thinking_block_open(&mut self) -> Vec<Value> {
if let Some(idx) = self.thinking_block_index {
if self
.open_blocks
.get(&idx)
.map(|value| value == "thinking")
.unwrap_or(false)
{
return Vec::new();
}
}
let idx = self.next_block_index;
self.next_block_index += 1;
self.thinking_block_index = Some(idx);
self.open_blocks.insert(idx, "thinking".to_string());
vec![json!({
"type": "content_block_start",
"index": idx,
"content_block": {"type": "thinking", "thinking": ""}
})]
}
pub(super) fn close_block(&mut self, idx: usize) -> Vec<Value> {
if self.open_blocks.remove(&idx).is_none() {
return Vec::new();
}
vec![json!({"type": "content_block_stop", "index": idx})]
}
pub(super) fn emit_text_delta(&mut self, text: &str) -> Vec<Value> {
if text.is_empty() {
return Vec::new();
}
let mut events = self.ensure_text_block_open();
let idx = self.text_block_index.unwrap_or_default();
events.push(json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "text_delta", "text": text}
}));
events
}
pub(super) fn emit_thinking_delta(&mut self, thinking: &str) -> Vec<Value> {
if thinking.is_empty() {
return Vec::new();
}
let mut events = self.ensure_thinking_block_open();
let idx = self.thinking_block_index.unwrap_or_default();
events.push(json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "thinking_delta", "thinking": thinking}
}));
events
}
pub(super) fn close_thinking_block(&mut self) -> Vec<Value> {
let Some(idx) = self.thinking_block_index else {
return Vec::new();
};
let mut events = vec![json!({
"type": "content_block_delta",
"index": idx,
"delta": {"type": "thinking_delta", "thinking": ""}
})];
events.extend(self.close_block(idx));
events
}
}

View File

@@ -1,327 +0,0 @@
use serde_json::{json, Value};
use crate::ai_pipeline::{
calculate_kiro_context_input_tokens, encode_kiro_sse_events, estimate_kiro_tokens,
find_kiro_real_thinking_end_tag, find_kiro_real_thinking_end_tag_at_buffer_end,
find_kiro_real_thinking_start_tag, KIRO_MAX_THINKING_BUFFER,
};
use crate::GatewayError;
use super::super::AwsEventFrame;
use super::super::KiroClaudeStreamState;
fn floor_char_boundary(text: &str, index: usize) -> usize {
let mut boundary = index.min(text.len());
while boundary > 0 && !text.is_char_boundary(boundary) {
boundary -= 1;
}
boundary
}
fn split_preserving_trailing_bytes(
buffer: &str,
trailing_bytes: usize,
) -> Option<(String, String)> {
if buffer.len() <= trailing_bytes {
return None;
}
let split = floor_char_boundary(buffer, buffer.len() - trailing_bytes);
if split == 0 {
return None;
}
Some((buffer[..split].to_string(), buffer[split..].to_string()))
}
impl KiroClaudeStreamState {
pub(super) fn process_frame(&mut self, frame: AwsEventFrame) -> Result<Vec<u8>, GatewayError> {
let message_type = frame.headers.message_type().unwrap_or("event");
match message_type {
"event" => self.process_event_frame(frame),
"exception" => self.process_exception_frame(frame),
"error" => self.process_error_frame(frame),
_ => Ok(Vec::new()),
}
}
pub(super) fn process_event_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, GatewayError> {
let event_type = frame.headers.event_type().unwrap_or_default();
let payload: Value = if frame.payload.is_empty() {
json!({})
} else {
serde_json::from_slice(&frame.payload).unwrap_or_else(|_| json!({}))
};
let payload_object = payload.as_object();
let mut events = Vec::new();
match event_type {
"assistantResponseEvent" => {
if let Some(content) = payload_object
.and_then(|value| value.get("content"))
.and_then(Value::as_str)
{
events.extend(self.process_assistant_response(content));
}
}
"toolUseEvent" => {
if let Some(payload_object) = payload_object {
let name = payload_object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let tool_use_id = payload_object
.get("toolUseId")
.or_else(|| payload_object.get("tool_use_id"))
.and_then(Value::as_str)
.unwrap_or_default();
let input_json = match payload_object.get("input") {
None | Some(Value::Null) => String::new(),
Some(Value::String(text)) => text.clone(),
Some(other) => serde_json::to_string(other)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
};
let stop = payload_object
.get("stop")
.and_then(Value::as_bool)
.unwrap_or(false);
events.extend(self.process_tool_use(name, tool_use_id, &input_json, stop));
}
}
"contextUsageEvent" => {
if let Some(percentage) = payload_object
.and_then(|value| value.get("contextUsagePercentage"))
.and_then(Value::as_f64)
{
self.context_input_tokens =
Some(calculate_kiro_context_input_tokens(percentage));
}
}
_ => {}
}
encode_kiro_sse_events(events).map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(super) fn process_exception_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, GatewayError> {
let exception_type = frame
.headers
.exception_type()
.unwrap_or("UnknownException")
.to_string();
if exception_type == "ContentLengthExceededException" {
self.stop_reason_override = Some("max_tokens".to_string());
return Ok(Vec::new());
}
self.emit_stream_error("upstream_exception", &exception_type)
}
pub(super) fn process_error_frame(
&mut self,
frame: AwsEventFrame,
) -> Result<Vec<u8>, GatewayError> {
let error_code = frame
.headers
.error_code()
.unwrap_or("UnknownError")
.to_string();
self.emit_stream_error("upstream_error", &error_code)
}
pub(super) fn process_assistant_response(&mut self, content: &str) -> Vec<Value> {
if content.is_empty() || content == self.last_content {
return Vec::new();
}
self.last_content = content.to_string();
self.output_tokens += estimate_kiro_tokens(content);
if !self.thinking_enabled {
return self.emit_text_delta(content);
}
self.thinking_buffer.push_str(content);
if self.thinking_buffer.len() > KIRO_MAX_THINKING_BUFFER {
let overflow = std::mem::take(&mut self.thinking_buffer);
if self.in_thinking_block {
let mut events = self.emit_thinking_delta(&overflow);
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
return events;
}
return self.emit_text_delta(&overflow);
}
let mut events = Vec::new();
loop {
if !self.in_thinking_block && !self.thinking_extracted {
if let Some(start_pos) = find_kiro_real_thinking_start_tag(&self.thinking_buffer) {
let before = self.thinking_buffer[..start_pos].to_string();
if !before.trim().is_empty() {
events.extend(self.emit_text_delta(&before));
}
self.in_thinking_block = true;
self.strip_thinking_leading_newline = true;
self.thinking_buffer =
self.thinking_buffer[start_pos + "<thinking>".len()..].to_string();
events.extend(self.ensure_thinking_block_open());
continue;
}
let keep = "<thinking>".len();
if let Some((safe, remaining)) =
split_preserving_trailing_bytes(&self.thinking_buffer, keep)
{
if !safe.trim().is_empty() {
events.extend(self.emit_text_delta(&safe));
self.thinking_buffer = remaining;
}
}
break;
}
if self.in_thinking_block {
if self.strip_thinking_leading_newline {
if self.thinking_buffer.starts_with('\n') {
self.thinking_buffer.remove(0);
self.strip_thinking_leading_newline = false;
} else if !self.thinking_buffer.is_empty() {
self.strip_thinking_leading_newline = false;
}
}
if let Some(end_pos) = find_kiro_real_thinking_end_tag(&self.thinking_buffer) {
let thinking_text = self.thinking_buffer[..end_pos].to_string();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
self.thinking_buffer =
self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
continue;
}
let keep = "</thinking>".len();
if let Some((safe, remaining)) =
split_preserving_trailing_bytes(&self.thinking_buffer, keep)
{
if !safe.is_empty() {
events.extend(self.emit_thinking_delta(&safe));
self.thinking_buffer = remaining;
}
}
break;
}
if !self.thinking_buffer.is_empty() {
let remaining = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_text_delta(&remaining));
}
break;
}
events
}
pub(super) fn process_tool_use(
&mut self,
name: &str,
tool_use_id: &str,
input_json: &str,
stop: bool,
) -> Vec<Value> {
if tool_use_id.is_empty() {
return Vec::new();
}
self.has_tool_use = true;
let mut events = Vec::new();
if self.thinking_enabled && self.in_thinking_block && !self.thinking_buffer.is_empty() {
if let Some(end_pos) =
find_kiro_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer)
{
let thinking_text = self.thinking_buffer[..end_pos].to_string();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
let remaining = self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
self.thinking_buffer.clear();
self.in_thinking_block = false;
self.thinking_extracted = true;
if !remaining.is_empty() {
events.extend(self.emit_text_delta(&remaining));
}
} else {
let thinking = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_thinking_delta(&thinking));
events.extend(self.close_thinking_block());
self.in_thinking_block = false;
self.thinking_extracted = true;
}
}
if self.thinking_enabled
&& !self.in_thinking_block
&& !self.thinking_extracted
&& !self.thinking_buffer.is_empty()
{
let buffered = std::mem::take(&mut self.thinking_buffer);
events.extend(self.emit_text_delta(&buffered));
}
if let Some(idx) = self.text_block_index.take() {
events.extend(self.close_block(idx));
}
let block_index = if let Some(block_index) = self.tool_block_indices.get(tool_use_id) {
*block_index
} else {
let block_index = self.next_block_index;
self.next_block_index += 1;
self.tool_block_indices
.insert(tool_use_id.to_string(), block_index);
block_index
};
if let std::collections::btree_map::Entry::Vacant(e) = self.open_blocks.entry(block_index) {
e.insert("tool_use".to_string());
events.push(json!({
"type": "content_block_start",
"index": block_index,
"content_block": {
"type": "tool_use",
"id": tool_use_id,
"name": name,
"input": {},
}
}));
}
if !input_json.is_empty() {
self.output_tokens += estimate_kiro_tokens(input_json);
events.push(json!({
"type": "content_block_delta",
"index": block_index,
"delta": {
"type": "input_json_delta",
"partial_json": input_json,
}
}));
}
if stop {
events.extend(self.close_block(block_index));
}
events
}
}

View File

@@ -1,96 +0,0 @@
use crate::ai_pipeline::{
build_kiro_final_message_sse_events, encode_kiro_sse_events,
find_kiro_real_thinking_end_tag_at_buffer_end,
};
use crate::GatewayError;
use super::super::KiroClaudeStreamState;
impl KiroClaudeStreamState {
pub(super) fn finalize(&mut self) -> Result<Vec<u8>, GatewayError> {
if self.thinking_enabled && !self.thinking_buffer.is_empty() {
let flush_events = if self.in_thinking_block {
if let Some(end_pos) =
find_kiro_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer)
{
let thinking_text = self.thinking_buffer[..end_pos].to_string();
let mut events = Vec::new();
if !thinking_text.is_empty() {
events.extend(self.emit_thinking_delta(&thinking_text));
}
events.extend(self.close_thinking_block());
let remaining =
self.thinking_buffer[end_pos + "</thinking>".len()..].to_string();
if !remaining.is_empty() {
events.extend(self.emit_text_delta(&remaining));
}
events
} else {
let mut events = self.emit_thinking_delta(&self.thinking_buffer.clone());
events.extend(self.close_thinking_block());
events
}
} else {
self.emit_text_delta(&self.thinking_buffer.clone())
};
self.thinking_buffer.clear();
self.in_thinking_block = false;
self.thinking_extracted = true;
let mut output = encode_kiro_sse_events(flush_events)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
for idx in self
.open_blocks
.keys()
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
{
output.extend(
encode_kiro_sse_events(self.close_block(idx))
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(self.final_message_bytes()?);
return Ok(output);
}
let mut output = Vec::new();
for idx in self
.open_blocks
.keys()
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
{
output.extend(
encode_kiro_sse_events(self.close_block(idx))
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(self.final_message_bytes()?);
Ok(output)
}
pub(super) fn final_message_bytes(&self) -> Result<Vec<u8>, GatewayError> {
let stop_reason = self.stop_reason_override.clone().unwrap_or_else(|| {
if self.has_tool_use {
"tool_use"
} else {
"end_turn"
}
.to_string()
});
let input_tokens = self
.context_input_tokens
.unwrap_or(self.estimated_input_tokens) as u64;
encode_kiro_sse_events(build_kiro_final_message_sse_events(
&stop_reason,
input_tokens as usize,
self.output_tokens,
))
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}

View File

@@ -1,129 +0,0 @@
use serde_json::Value;
use uuid::Uuid;
use crate::ai_pipeline::{
build_kiro_initial_sse_events, build_kiro_stream_error_sse_events, encode_kiro_sse_events,
};
use crate::GatewayError;
use super::super::{EventStreamDecoder, KiroClaudeStreamState, KiroToClaudeCliStreamState};
impl KiroToClaudeCliStreamState {
pub(crate) fn new(report_context: &Value) -> Self {
Self {
decoder: EventStreamDecoder::default(),
state: KiroClaudeStreamState::new(report_context),
started: false,
}
}
pub(crate) fn push_chunk(
&mut self,
_report_context: &Value,
chunk: &[u8],
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
if !self.started {
self.started = true;
output.extend(self.state.generate_initial_bytes()?);
}
if let Err(err) = self.decoder.feed(chunk) {
output.extend(
self.state
.emit_stream_error("upstream_stream_error", &err)?,
);
return Ok(output);
}
match self.decoder.decode_available() {
Ok(frames) => {
for frame in frames {
output.extend(self.state.process_frame(frame)?);
}
}
Err(err) => {
output.extend(
self.state
.emit_stream_error("upstream_stream_error", &err)?,
);
}
}
Ok(output)
}
pub(crate) fn finish(&mut self, _report_context: &Value) -> Result<Vec<u8>, GatewayError> {
if !self.started || self.state.had_error {
return Ok(Vec::new());
}
self.state.finalize()
}
}
impl KiroClaudeStreamState {
pub(super) fn new(report_context: &Value) -> Self {
let model = report_context
.get("mapped_model")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.or_else(|| {
report_context
.get("model")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
})
.unwrap_or("unknown")
.to_string();
let thinking_enabled = report_context
.get("original_request_body")
.and_then(Value::as_object)
.and_then(|body| body.get("thinking"))
.and_then(Value::as_object)
.and_then(|thinking| thinking.get("type"))
.and_then(Value::as_str)
.map(|value| {
value.trim().eq_ignore_ascii_case("enabled")
|| value.trim().eq_ignore_ascii_case("adaptive")
})
.unwrap_or(false);
let estimated_input_tokens = report_context
.get("input_tokens")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
Self {
model,
thinking_enabled,
estimated_input_tokens,
message_id: format!("msg_{}", Uuid::new_v4().simple()),
..Self::default()
}
}
pub(super) fn generate_initial_bytes(&mut self) -> Result<Vec<u8>, GatewayError> {
let events = build_kiro_initial_sse_events(
&self.message_id,
&self.model,
self.estimated_input_tokens,
);
let mut events = events;
if !self.thinking_enabled {
events.extend(self.ensure_text_block_open());
}
encode_kiro_sse_events(events).map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(super) fn emit_stream_error(
&mut self,
error_type: &str,
message: &str,
) -> Result<Vec<u8>, GatewayError> {
if self.had_error {
return Ok(Vec::new());
}
self.had_error = true;
encode_kiro_sse_events(build_kiro_stream_error_sse_events(error_type, message))
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}

View File

@@ -1,166 +0,0 @@
use aether_ai_pipeline::adaptation::kiro_stream::kiro_crc32 as crc32;
use serde_json::{json, Value};
use super::KiroToClaudeCliStreamState;
fn encode_string_header(name: &str, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(name.len() as u8);
out.extend_from_slice(name.as_bytes());
out.push(7);
out.extend_from_slice(&(value.len() as u16).to_be_bytes());
out.extend_from_slice(value.as_bytes());
out
}
fn encode_event_frame(message_type: &str, event_type: Option<&str>, payload: &Value) -> Vec<u8> {
let mut headers = encode_string_header(":message-type", message_type);
if let Some(event_type) = event_type {
headers.extend_from_slice(&encode_string_header(":event-type", event_type));
}
let payload_bytes = serde_json::to_vec(payload).expect("payload should encode");
encode_frame(headers, payload_bytes)
}
fn encode_frame(headers: Vec<u8>, payload: Vec<u8>) -> Vec<u8> {
let total_len = 12 + headers.len() + payload.len() + 4;
let header_len = headers.len();
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(&(total_len as u32).to_be_bytes());
out.extend_from_slice(&(header_len as u32).to_be_bytes());
let prelude_crc = crc32(&out[..8]);
out.extend_from_slice(&prelude_crc.to_be_bytes());
out.extend_from_slice(&headers);
out.extend_from_slice(&payload);
let message_crc = crc32(&out);
out.extend_from_slice(&message_crc.to_be_bytes());
out
}
fn kiro_report_context(thinking_enabled: bool) -> Value {
let mut context = json!({
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"mapped_model": "claude-sonnet-4.5"
});
if thinking_enabled {
context["original_request_body"] = json!({
"thinking": {
"type": "enabled"
}
});
}
context
}
#[test]
fn kiro_stream_rewriter_converts_text_events_to_claude_sse() {
let report_context = kiro_report_context(false);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = [
encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Hello from Kiro"}),
),
encode_event_frame(
"event",
Some("contextUsageEvent"),
&json!({"contextUsagePercentage": 1.0}),
),
]
.concat();
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("event: message_start"));
assert!(text.contains("\"type\":\"content_block_delta\""));
assert!(text.contains("Hello from Kiro"));
assert!(text.contains("\"stop_reason\":\"end_turn\""));
assert!(text.contains("\"input_tokens\":2000"));
}
#[test]
fn kiro_stream_rewriter_converts_tool_use_to_claude_events() {
let report_context = kiro_report_context(false);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = [
encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "Need a tool."}),
),
encode_event_frame(
"event",
Some("toolUseEvent"),
&json!({
"name": "get_weather",
"toolUseId": "tool_123",
"input": {"city": "SF"},
"stop": true
}),
),
]
.concat();
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"tool_use\""));
assert!(text.contains("\"id\":\"tool_123\""));
assert!(text.contains("\"name\":\"get_weather\""));
assert!(text.contains("\"partial_json\":\"{\\\"city\\\":\\\"SF\\\"}\""));
assert!(text.contains("\"stop_reason\":\"tool_use\""));
}
#[test]
fn kiro_stream_rewriter_handles_multibyte_text_without_thinking_tag() {
let report_context = kiro_report_context(true);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "\n\n你好!有"}),
);
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"text_delta\""));
assert!(text.contains("你好!有"));
}
#[test]
fn kiro_stream_rewriter_handles_multibyte_text_inside_thinking_block() {
let report_context = kiro_report_context(true);
let mut rewriter = KiroToClaudeCliStreamState::new(&report_context);
let chunk = encode_event_frame(
"event",
Some("assistantResponseEvent"),
&json!({"content": "<thinking>\n\n你好!有"}),
);
let first = rewriter
.push_chunk(&report_context, &chunk)
.expect("rewrite should succeed");
let rest = rewriter
.finish(&report_context)
.expect("finish should succeed");
let text = String::from_utf8([first, rest].concat()).expect("utf8 should decode");
assert!(text.contains("\"type\":\"thinking_delta\""));
assert!(text.contains("你好!有"));
}

View File

@@ -1,17 +0,0 @@
#[path = "private_envelope/stream.rs"]
mod stream;
#[path = "private_envelope/sync.rs"]
mod sync;
#[cfg(test)]
#[path = "private_envelope/tests.rs"]
mod tests;
pub(crate) use self::stream::{
maybe_build_provider_private_stream_normalizer, ProviderPrivateStreamNormalizer,
};
pub(crate) use self::sync::maybe_normalize_provider_private_sync_report_payload;
pub(crate) use crate::ai_pipeline::{
normalize_provider_private_report_context, normalize_provider_private_response_value,
provider_private_response_allows_sync_finalize, stream_body_contains_error_event,
transform_provider_private_stream_line,
};

View File

@@ -1,96 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::adaptation::KiroToClaudeCliStreamState;
use crate::ai_pipeline::{provider_adaptation_descriptor_for_envelope, KIRO_ENVELOPE_NAME};
use crate::GatewayError;
use super::transform_provider_private_stream_line;
enum ProviderPrivateStreamNormalizeMode {
EnvelopeUnwrap,
KiroToClaudeCli(KiroToClaudeCliStreamState),
}
pub(crate) struct ProviderPrivateStreamNormalizer<'a> {
report_context: &'a Value,
buffered: Vec<u8>,
mode: ProviderPrivateStreamNormalizeMode,
}
pub(crate) fn maybe_build_provider_private_stream_normalizer<'a>(
report_context: Option<&'a Value>,
) -> Option<ProviderPrivateStreamNormalizer<'a>> {
let report_context = report_context?;
if !report_context
.get("has_envelope")
.and_then(Value::as_bool)
.unwrap_or(false)
{
return None;
}
let envelope_name = report_context
.get("envelope_name")
.and_then(Value::as_str)
.unwrap_or_default();
let provider_api_format = report_context
.get("provider_api_format")
.and_then(Value::as_str)
.unwrap_or_default();
let descriptor =
provider_adaptation_descriptor_for_envelope(envelope_name, provider_api_format)?;
let mode = if descriptor
.envelope_name
.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME)
{
ProviderPrivateStreamNormalizeMode::KiroToClaudeCli(KiroToClaudeCliStreamState::new(
report_context,
))
} else if descriptor.unwraps_response_envelope {
ProviderPrivateStreamNormalizeMode::EnvelopeUnwrap
} else {
return None;
};
Some(ProviderPrivateStreamNormalizer {
report_context,
buffered: Vec::new(),
mode,
})
}
impl ProviderPrivateStreamNormalizer<'_> {
pub(crate) fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<u8>, GatewayError> {
match &mut self.mode {
ProviderPrivateStreamNormalizeMode::KiroToClaudeCli(state) => {
state.push_chunk(self.report_context, chunk)
}
ProviderPrivateStreamNormalizeMode::EnvelopeUnwrap => {
self.buffered.extend_from_slice(chunk);
let mut output = Vec::new();
while let Some(line_end) = self.buffered.iter().position(|byte| *byte == b'\n') {
let line = self.buffered.drain(..=line_end).collect::<Vec<_>>();
output.extend(
transform_provider_private_stream_line(self.report_context, line)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
Ok(output)
}
}
}
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
match &mut self.mode {
ProviderPrivateStreamNormalizeMode::KiroToClaudeCli(state) => {
state.finish(self.report_context)
}
ProviderPrivateStreamNormalizeMode::EnvelopeUnwrap => {
if self.buffered.is_empty() {
return Ok(Vec::new());
}
let line = std::mem::take(&mut self.buffered);
transform_provider_private_stream_line(self.report_context, line)
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}
}
}

View File

@@ -1,39 +0,0 @@
use serde_json::json;
use super::{
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
};
#[test]
fn normalizes_supported_private_report_context() {
let report_context = json!({
"has_envelope": true,
"envelope_name": "antigravity:v1internal",
"provider_api_format": "gemini:generate_content",
});
let normalized = normalize_provider_private_report_context(Some(&report_context))
.expect("context should normalize");
assert_eq!(normalized["has_envelope"], json!(false));
assert!(normalized.get("envelope_name").is_none());
}
#[test]
fn private_stream_normalizer_unwraps_antigravity_stream() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"mapped_model": "claude-sonnet-4-5",
});
let mut normalizer = maybe_build_provider_private_stream_normalizer(Some(&report_context))
.expect("normalizer should exist");
let output = normalizer
.push_chunk(
b"data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\"},\"responseId\":\"resp_123\"}\n\n",
)
.expect("unwrap should succeed");
let output_text = String::from_utf8(output).expect("text should decode");
assert!(output_text.contains("\"_v1internal_response_id\":\"resp_123\""));
assert!(output_text.contains("\"id\":\"call_get_weather_0\""));
}

View File

@@ -1,22 +0,0 @@
use tracing::warn;
use crate::ai_pipeline::{
generic_decision_missing_exact_provider_request as generic_decision_missing_exact_provider_request_impl,
GatewayControlSyncDecisionResponse,
};
pub(crate) fn generic_decision_missing_exact_provider_request(
payload: &GatewayControlSyncDecisionResponse,
) -> bool {
if !generic_decision_missing_exact_provider_request_impl(payload) {
return false;
}
warn!(
decision_kind = payload.decision_kind.as_deref().unwrap_or_default(),
provider_api_format = payload.provider_api_format.as_deref().unwrap_or_default(),
client_api_format = payload.client_api_format.as_deref().unwrap_or_default(),
"gateway generic decision missing exact provider request; falling back to plan"
);
true
}

View File

@@ -1,41 +0,0 @@
pub(crate) mod control_payloads;
pub(crate) use crate::ai_pipeline::{
core_error_background_report_kind, core_error_default_client_api_format,
core_success_background_report_kind, implicit_sync_finalize_report_kind,
ExecutionRuntimeAuthContext, GatewayControlPlanRequest, GatewayControlPlanResponse,
GatewayControlSyncDecisionResponse, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND,
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CLI_SYNC_ERROR_REPORT_KIND,
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CLI_SYNC_PLAN_KIND,
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND,
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND,
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub(crate) use control_payloads::generic_decision_missing_exact_provider_request;

View File

@@ -1,156 +0,0 @@
#[cfg(test)]
pub(crate) use crate::ai_pipeline::core_success_background_report_kind;
pub(crate) use crate::ai_pipeline::{
build_core_error_body_for_client_format, core_error_background_report_kind,
core_error_default_client_api_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
pub(crate) use crate::ai_pipeline::{
request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_direct_auth, request_conversion_enabled_for_transport,
request_conversion_kind, request_conversion_requires_enable_flag,
request_conversion_transport_supported, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};
#[cfg(test)]
mod tests {
use super::{
request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_kind, sync_chat_response_conversion_kind,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
};
#[test]
fn request_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
request_conversion_kind("openai:chat", "openai:responses"),
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
request_conversion_kind("openai:responses", "openai:chat"),
Some(RequestConversionKind::ToOpenAIChat)
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
request_conversion_kind("openai:responses:compact", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("openai:chat", "openai:responses:compact"),
None
);
assert_eq!(
request_conversion_kind("claude:messages", "claude:messages"),
None
);
}
#[test]
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
sync_chat_response_conversion_kind("openai:chat", "claude:messages"),
Some(SyncChatResponseConversionKind::ToClaudeChat)
);
assert_eq!(
sync_chat_response_conversion_kind("claude:messages", "gemini:generate_content"),
Some(SyncChatResponseConversionKind::ToGeminiChat)
);
assert_eq!(
sync_chat_response_conversion_kind("gemini:generate_content", "openai:chat"),
Some(SyncChatResponseConversionKind::ToOpenAIChat)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "gemini:generate_content"),
Some(SyncCliResponseConversionKind::ToGeminiCli)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:messages", "openai:responses"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:messages", "openai:responses:compact"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses:compact", "claude:messages"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("gemini:generate_content", "claude:messages"),
Some(SyncCliResponseConversionKind::ToClaudeCli)
);
}
#[test]
fn request_candidate_registry_excludes_compact_as_cross_format_target() {
assert_eq!(
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"openai:responses",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"openai:chat",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("claude:messages", false),
vec![
"claude:messages",
"openai:chat",
"openai:responses",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
Vec::<&'static str>::new()
);
}
#[test]
fn request_candidate_registry_prefers_same_kind_before_same_family_fallbacks() {
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:responses"),
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
None
);
}
}

View File

@@ -1,501 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
use crate::ai_pipeline::adaptation::KiroToClaudeCliStreamState;
use crate::ai_pipeline::finalize::sse::encode_json_sse;
use crate::ai_pipeline::finalize::standard::StreamingStandardConversionState;
use crate::ai_pipeline::{resolve_finalize_stream_rewrite_mode, FinalizeStreamRewriteMode};
use crate::GatewayError;
enum RewriteMode {
EnvelopeUnwrap,
OpenAiImage(OpenAiImageStreamState),
Standard(StreamingStandardConversionState),
KiroToClaudeCli(KiroToClaudeCliStreamState),
KiroToClaudeCliThenStandard {
kiro: KiroToClaudeCliStreamState,
standard: StreamingStandardConversionState,
},
}
pub(crate) struct LocalStreamRewriter<'a> {
report_context: &'a Value,
buffered: Vec<u8>,
mode: RewriteMode,
}
pub(crate) fn maybe_build_local_stream_rewriter<'a>(
report_context: Option<&'a Value>,
) -> Option<LocalStreamRewriter<'a>> {
let report_context = report_context?;
let mode = match resolve_finalize_stream_rewrite_mode(report_context)? {
FinalizeStreamRewriteMode::EnvelopeUnwrap => RewriteMode::EnvelopeUnwrap,
FinalizeStreamRewriteMode::OpenAiImage => {
RewriteMode::OpenAiImage(OpenAiImageStreamState::default())
}
FinalizeStreamRewriteMode::Standard => {
RewriteMode::Standard(StreamingStandardConversionState::default())
}
FinalizeStreamRewriteMode::KiroToClaudeCli => {
RewriteMode::KiroToClaudeCli(KiroToClaudeCliStreamState::new(report_context))
}
FinalizeStreamRewriteMode::KiroToClaudeCliThenStandard => {
RewriteMode::KiroToClaudeCliThenStandard {
kiro: KiroToClaudeCliStreamState::new(report_context),
standard: StreamingStandardConversionState::default(),
}
}
};
Some(LocalStreamRewriter {
report_context,
buffered: Vec::new(),
mode,
})
}
impl LocalStreamRewriter<'_> {
pub(crate) fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<u8>, GatewayError> {
if let RewriteMode::OpenAiImage(state) = &mut self.mode {
return state.push_chunk(self.report_context, chunk);
}
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
return state.push_chunk(self.report_context, chunk);
}
if let RewriteMode::KiroToClaudeCliThenStandard { kiro, standard } = &mut self.mode {
let claude_bytes = kiro.push_chunk(self.report_context, chunk)?;
return transform_standard_bytes(standard, self.report_context, claude_bytes);
}
self.buffered.extend_from_slice(chunk);
let mut output = Vec::new();
while let Some(line_end) = self.buffered.iter().position(|byte| *byte == b'\n') {
let line = self.buffered.drain(..=line_end).collect::<Vec<_>>();
output.extend(self.transform_line(line)?);
}
Ok(output)
}
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
if let RewriteMode::OpenAiImage(state) = &mut self.mode {
return state.finish(self.report_context);
}
if let RewriteMode::KiroToClaudeCli(state) = &mut self.mode {
return state.finish(self.report_context);
}
if let RewriteMode::KiroToClaudeCliThenStandard { kiro, standard } = &mut self.mode {
let mut output = transform_standard_bytes(
standard,
self.report_context,
kiro.finish(self.report_context)?,
)?;
output.extend(standard.finish(self.report_context)?);
return Ok(output);
}
if self.buffered.is_empty() {
match &mut self.mode {
RewriteMode::Standard(state) => return state.finish(self.report_context),
RewriteMode::OpenAiImage(_) => {}
RewriteMode::KiroToClaudeCli(_) => {}
RewriteMode::KiroToClaudeCliThenStandard { .. } => {}
RewriteMode::EnvelopeUnwrap => {}
}
return Ok(Vec::new());
}
let line = std::mem::take(&mut self.buffered);
let mut output = self.transform_line(line)?;
match &mut self.mode {
RewriteMode::Standard(state) => {
output.extend(state.finish(self.report_context)?);
}
RewriteMode::OpenAiImage(_) => {}
RewriteMode::KiroToClaudeCli(_) => {}
RewriteMode::KiroToClaudeCliThenStandard { .. } => {}
RewriteMode::EnvelopeUnwrap => {}
}
Ok(output)
}
fn transform_line(&mut self, line: Vec<u8>) -> Result<Vec<u8>, GatewayError> {
match &mut self.mode {
RewriteMode::EnvelopeUnwrap => transform_envelope_line(self.report_context, line)
.map_err(|err| GatewayError::Internal(err.to_string())),
RewriteMode::OpenAiImage(_) => Ok(Vec::new()),
RewriteMode::Standard(state) => state.transform_line(self.report_context, line),
RewriteMode::KiroToClaudeCli(_) => Ok(Vec::new()),
RewriteMode::KiroToClaudeCliThenStandard { .. } => Ok(Vec::new()),
}
}
}
fn transform_standard_bytes(
standard: &mut StreamingStandardConversionState,
report_context: &Value,
bytes: Vec<u8>,
) -> Result<Vec<u8>, GatewayError> {
if bytes.is_empty() {
return Ok(Vec::new());
}
let mut output = Vec::new();
for line in bytes.split_inclusive(|byte| *byte == b'\n') {
output.extend(standard.transform_line(report_context, line.to_vec())?);
}
Ok(output)
}
#[derive(Default)]
struct OpenAiImageStreamState {
buffered: Vec<u8>,
latest_image: Option<OpenAiImageFrame>,
emitted_partial_count: u64,
saw_upstream_partial: bool,
emitted_failure: bool,
}
#[derive(Clone)]
struct OpenAiImageFrame {
b64_json: String,
}
impl OpenAiImageStreamState {
fn push_chunk(
&mut self,
report_context: &Value,
chunk: &[u8],
) -> Result<Vec<u8>, GatewayError> {
self.buffered.extend_from_slice(chunk);
let mut output = Vec::new();
while let Some(block_end) = find_sse_block_end(&self.buffered) {
let block = self.buffered.drain(..block_end).collect::<Vec<_>>();
output.extend(self.transform_block(report_context, &block)?);
drain_sse_separator(&mut self.buffered);
}
Ok(output)
}
fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, GatewayError> {
if self.buffered.is_empty() {
return Ok(Vec::new());
}
let block = std::mem::take(&mut self.buffered);
self.transform_block(report_context, &block)
}
fn transform_block(
&mut self,
report_context: &Value,
block: &[u8],
) -> Result<Vec<u8>, GatewayError> {
let text =
std::str::from_utf8(block).map_err(|err| GatewayError::Internal(err.to_string()))?;
let mut event_name = None::<String>;
let mut data_lines = Vec::new();
for raw_line in text.lines() {
let line = raw_line.trim_end_matches('\r');
if let Some(value) = line.strip_prefix("event:") {
event_name = Some(value.trim().to_string());
} else if let Some(value) = line.strip_prefix("data:") {
data_lines.push(value.trim().to_string());
}
}
let data = data_lines.join("\n");
if data.is_empty() || data == "[DONE]" {
return Ok(Vec::new());
}
let event: Value =
serde_json::from_str(&data).map_err(|err| GatewayError::Internal(err.to_string()))?;
let event_type = event
.get("type")
.and_then(Value::as_str)
.or(event_name.as_deref())
.unwrap_or_default();
match event_type {
"error" | "response.failed" => self.handle_failed(report_context, &event),
"response.image_generation_call.partial_image" => {
self.handle_image_generation_partial(report_context, &event)
}
"response.output_item.done" => self.handle_output_item_done(report_context, &event),
"response.completed" => self.handle_completed(report_context, &event),
_ => Ok(Vec::new()),
}
}
fn handle_image_generation_partial(
&mut self,
report_context: &Value,
event: &Value,
) -> Result<Vec<u8>, GatewayError> {
if self.emitted_failure {
return Ok(Vec::new());
}
if requested_partial_images(report_context) == 0 {
return Ok(Vec::new());
}
let Some(result) = event
.get("partial_image_b64")
.or_else(|| event.get("b64_json"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(Vec::new());
};
let partial_image_index = event
.get("partial_image_index")
.or_else(|| event.get("output_index"))
.and_then(Value::as_u64)
.unwrap_or(self.emitted_partial_count);
self.emitted_partial_count = self
.emitted_partial_count
.max(partial_image_index.saturating_add(1));
self.saw_upstream_partial = true;
self.latest_image = Some(OpenAiImageFrame {
b64_json: result.to_string(),
});
encode_json_sse(
Some(image_partial_event_name(report_context)),
&serde_json::json!({
"type": image_partial_event_name(report_context),
"b64_json": result,
"partial_image_index": partial_image_index,
}),
)
}
fn handle_output_item_done(
&mut self,
report_context: &Value,
event: &Value,
) -> Result<Vec<u8>, GatewayError> {
if self.emitted_failure {
return Ok(Vec::new());
}
let Some(item) = event.get("item").and_then(Value::as_object) else {
return Ok(Vec::new());
};
if item.get("type").and_then(Value::as_str) != Some("image_generation_call") {
return Ok(Vec::new());
}
let Some(result) = item.get("result").and_then(Value::as_str).map(str::trim) else {
return Ok(Vec::new());
};
if result.is_empty() {
return Ok(Vec::new());
}
self.latest_image = Some(OpenAiImageFrame {
b64_json: result.to_string(),
});
if requested_partial_images(report_context) == 0 || self.saw_upstream_partial {
return Ok(Vec::new());
}
let partial_image_index = event
.get("output_index")
.and_then(Value::as_u64)
.unwrap_or(self.emitted_partial_count);
self.emitted_partial_count = partial_image_index.saturating_add(1);
encode_json_sse(
Some(image_partial_event_name(report_context)),
&serde_json::json!({
"type": image_partial_event_name(report_context),
"b64_json": result,
"partial_image_index": partial_image_index,
}),
)
}
fn handle_completed(
&mut self,
report_context: &Value,
event: &Value,
) -> Result<Vec<u8>, GatewayError> {
if self.emitted_failure {
return Ok(Vec::new());
}
if self.latest_image.is_none() {
if let Some(result) = completed_response_image_result(event) {
self.latest_image = Some(OpenAiImageFrame {
b64_json: result.to_string(),
});
}
}
let Some(latest_image) = self.latest_image.clone() else {
return Ok(Vec::new());
};
let usage = event
.get("response")
.and_then(Value::as_object)
.and_then(|response| {
response
.get("tool_usage")
.and_then(|value| value.get("image_gen"))
.cloned()
.or_else(|| response.get("usage").cloned())
})
.unwrap_or(Value::Null);
encode_json_sse(
Some(image_completed_event_name(report_context)),
&serde_json::json!({
"type": image_completed_event_name(report_context),
"b64_json": latest_image.b64_json,
"usage": usage,
}),
)
}
fn handle_failed(
&mut self,
report_context: &Value,
event: &Value,
) -> Result<Vec<u8>, GatewayError> {
if self.emitted_failure {
return Ok(Vec::new());
}
self.emitted_failure = true;
let error = image_failure_error(event);
encode_json_sse(
Some(image_failed_event_name(report_context)),
&serde_json::json!({
"type": image_failed_event_name(report_context),
"error": error,
}),
)
}
}
fn image_failure_error(event: &Value) -> Value {
let mut error = event
.get("error")
.or_else(|| event.get("response").and_then(|value| value.get("error")))
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
if !error.contains_key("message") {
if let Some(message) = event
.get("message")
.and_then(Value::as_str)
.or_else(|| {
event
.get("response")
.and_then(|value| value.get("error"))
.and_then(|value| value.get("message"))
.and_then(Value::as_str)
})
.map(str::trim)
.filter(|value| !value.is_empty())
{
error.insert("message".to_string(), Value::String(message.to_string()));
}
}
if !error.contains_key("code") {
if let Some(code) = event
.get("code")
.or_else(|| {
event
.get("response")
.and_then(|value| value.get("error"))
.and_then(|value| value.get("code"))
})
.cloned()
{
error.insert("code".to_string(), code);
}
}
if !error.contains_key("type") {
let inferred_type = error
.get("code")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.unwrap_or("upstream_error");
error.insert("type".to_string(), Value::String(inferred_type.to_string()));
}
if !error.contains_key("message") {
error.insert(
"message".to_string(),
Value::String("Image generation failed".to_string()),
);
}
Value::Object(error)
}
fn completed_response_image_result(event: &Value) -> Option<&str> {
event
.get("response")
.and_then(|value| value.get("output"))
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|item| item.get("type").and_then(Value::as_str) == Some("image_generation_call"))
.filter_map(|item| item.get("result").and_then(Value::as_str))
.map(str::trim)
.find(|value| !value.is_empty())
}
fn requested_partial_images(report_context: &Value) -> u64 {
report_context
.get("image_request")
.and_then(|value| value.get("partial_images"))
.and_then(Value::as_u64)
.unwrap_or(0)
}
fn image_partial_event_name(report_context: &Value) -> &'static str {
if image_request_operation(report_context) == Some("edit") {
"image_edit.partial_image"
} else {
"image_generation.partial_image"
}
}
fn image_completed_event_name(report_context: &Value) -> &'static str {
if image_request_operation(report_context) == Some("edit") {
"image_edit.completed"
} else {
"image_generation.completed"
}
}
fn image_failed_event_name(report_context: &Value) -> &'static str {
if image_request_operation(report_context) == Some("edit") {
"image_edit.failed"
} else {
"image_generation.failed"
}
}
fn image_request_operation(report_context: &Value) -> Option<&str> {
report_context
.get("image_request")
.and_then(|value| value.get("operation"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn find_sse_block_end(buffer: &[u8]) -> Option<usize> {
buffer
.windows(2)
.position(|window| window == b"\n\n")
.map(|index| index + 2)
.or_else(|| {
buffer
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|index| index + 4)
})
}
fn drain_sse_separator(buffer: &mut Vec<u8>) {
while matches!(buffer.first(), Some(b'\n' | b'\r')) {
buffer.remove(0);
}
}
#[cfg(test)]
#[path = "../tests_stream.rs"]
mod tests;

View File

@@ -1,246 +0,0 @@
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT;
use crate::ai_pipeline::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::{usage::GatewaySyncReportRequest, GatewayError};
use base64::Engine as _;
pub(crate) use crate::ai_pipeline::finalize::common::{
build_local_success_outcome, build_local_success_outcome_with_conversion_report,
local_finalize_allows_envelope, unwrap_local_finalize_response_value,
LocalCoreSyncFinalizeOutcome,
};
pub(crate) use crate::ai_pipeline::finalize::standard::{
maybe_build_standard_sync_finalize_product_from_normalized_payload,
StandardSyncFinalizeNormalizedProduct,
};
pub(crate) use crate::ai_pipeline::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
};
pub(crate) use crate::ai_pipeline::{
convert_claude_chat_response_to_openai_chat, convert_claude_response_to_openai_responses,
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
};
pub(crate) fn maybe_build_local_core_sync_finalize_response(
trace_id: &str,
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
if let Some(outcome) =
maybe_build_local_openai_image_sync_finalize_response(trace_id, decision, payload)?
{
return Ok(Some(outcome));
}
let Some(normalized_payload) =
crate::ai_pipeline::adaptation::private_envelope::maybe_normalize_provider_private_sync_report_payload(payload)?
else {
return Ok(None);
};
let payload = &normalized_payload;
let Some(report_context) = payload.report_context.as_ref() else {
return Ok(None);
};
if !local_finalize_allows_envelope(report_context) {
return Ok(None);
}
let Some(product) = maybe_build_standard_sync_finalize_product_from_normalized_payload(
payload.report_kind.as_str(),
payload.status_code,
Some(report_context),
payload.body_json.as_ref(),
payload.body_base64.as_deref(),
)
.map_err(GatewayError::from)?
else {
return Ok(None);
};
match product {
StandardSyncFinalizeNormalizedProduct::SuccessBody(body_json) => {
let Some(body_json) = unwrap_local_finalize_response_value(body_json, report_context)
else {
return Ok(None);
};
Ok(Some(build_local_success_outcome(
trace_id, decision, payload, body_json,
)?))
}
StandardSyncFinalizeNormalizedProduct::CrossFormat(product) => {
let Some(provider_body_json) =
unwrap_local_finalize_response_value(product.provider_body_json, report_context)
else {
return Ok(None);
};
Ok(Some(build_local_success_outcome_with_conversion_report(
trace_id,
decision,
payload,
product.client_body_json,
provider_body_json,
)?))
}
}
}
fn maybe_build_local_openai_image_sync_finalize_response(
trace_id: &str,
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
if payload.report_kind != "openai_image_sync_finalize" || payload.status_code >= 400 {
return Ok(None);
}
let Some(report_context) = payload.report_context.as_ref() else {
return Ok(None);
};
if report_context
.get("client_api_format")
.and_then(serde_json::Value::as_str)
.map(str::trim)
!= Some("openai:image")
{
return Ok(None);
}
let Some(body_base64) = payload.body_base64.as_deref() else {
return Ok(None);
};
let default_output_format = report_context
.get("image_request")
.and_then(|value| value.get("output_format"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(CODEX_OPENAI_IMAGE_DEFAULT_OUTPUT_FORMAT);
let body_bytes = base64::engine::general_purpose::STANDARD
.decode(body_base64)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let text =
std::str::from_utf8(&body_bytes).map_err(|err| GatewayError::Internal(err.to_string()))?;
let mut created = None;
let mut completed_response = None;
let mut images = Vec::new();
for raw_block in text.split("\n\n") {
let block = raw_block.trim();
if block.is_empty() {
continue;
}
let data_line = block
.lines()
.find_map(|line| line.trim().strip_prefix("data:").map(str::trim));
let Some(data_line) = data_line else {
continue;
};
if data_line.is_empty() || data_line == "[DONE]" {
continue;
}
let event: serde_json::Value = serde_json::from_str(data_line)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
match event
.get("type")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
{
"response.created" => {
created = event
.get("response")
.and_then(|value| value.get("created_at"))
.and_then(serde_json::Value::as_i64)
.or(created);
}
"response.output_item.done" => {
let Some(item) = event.get("item").and_then(serde_json::Value::as_object) else {
continue;
};
if item.get("type").and_then(serde_json::Value::as_str)
!= Some("image_generation_call")
{
continue;
}
let Some(result) = item.get("result").and_then(serde_json::Value::as_str) else {
continue;
};
images.push(serde_json::json!({
"b64_json": result,
"output_format": item.get("output_format").cloned().unwrap_or(serde_json::Value::String(default_output_format.to_string())),
"revised_prompt": item.get("revised_prompt").cloned().unwrap_or(serde_json::Value::Null),
}));
}
"response.completed" => {
completed_response = event
.get("response")
.and_then(serde_json::Value::as_object)
.cloned();
}
_ => {}
}
}
if images.is_empty() {
return Ok(None);
}
let completed_response = completed_response.unwrap_or_default();
let provider_usage = completed_response
.get("tool_usage")
.and_then(|value| value.get("image_gen"))
.cloned()
.or_else(|| completed_response.get("usage").cloned());
let provider_body_json = serde_json::json!({
"id": completed_response.get("id").cloned().unwrap_or(serde_json::Value::Null),
"object": "response",
"model": completed_response.get("model").cloned().unwrap_or(serde_json::Value::Null),
"status": completed_response.get("status").cloned().unwrap_or(serde_json::Value::String("completed".to_string())),
"usage": provider_usage,
"tool_usage": completed_response.get("tool_usage").cloned().unwrap_or(serde_json::Value::Null),
"output": images
.iter()
.map(|image| serde_json::json!({
"type": "image_generation_call",
"output_format": image.get("output_format").cloned().unwrap_or(serde_json::Value::Null),
"revised_prompt": image.get("revised_prompt").cloned().unwrap_or(serde_json::Value::Null),
}))
.collect::<Vec<_>>(),
});
let client_images = images
.iter()
.map(|image| {
let revised_prompt = image
.get("revised_prompt")
.cloned()
.unwrap_or(serde_json::Value::Null);
let b64_json = image
.get("b64_json")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let output_format = image
.get("output_format")
.and_then(serde_json::Value::as_str)
.unwrap_or(default_output_format);
serde_json::json!({
"b64_json": b64_json,
"revised_prompt": revised_prompt,
})
})
.collect::<Vec<_>>();
let client_body_json = serde_json::json!({
"created": created.unwrap_or_default(),
"data": client_images,
"usage": provider_body_json.get("usage").cloned().unwrap_or(serde_json::Value::Null),
});
Ok(Some(build_local_success_outcome_with_conversion_report(
trace_id,
decision,
payload,
client_body_json,
provider_body_json,
)?))
}
#[cfg(test)]
#[path = "../tests_sync.rs"]
mod tests;

View File

@@ -1,564 +0,0 @@
use aether_contracts::{ExecutionStreamTerminalSummary, StandardizedUsage};
use serde_json::{json, Value};
use crate::ai_pipeline::finalize::sse::encode_json_sse;
use crate::ai_pipeline::{
convert_claude_response_to_openai_responses, convert_gemini_response_to_openai_responses,
convert_openai_chat_response_to_openai_responses, ClaudeClientEmitter, GeminiClientEmitter,
OpenAIChatClientEmitter, OpenAIResponsesClientEmitter, OpenAIResponsesProviderState,
};
use crate::GatewayError;
pub(crate) struct SyncToStreamBridgeOutcome {
pub(crate) sse_body: Vec<u8>,
pub(crate) terminal_summary: Option<ExecutionStreamTerminalSummary>,
}
pub(crate) fn maybe_bridge_standard_sync_json_to_stream(
provider_body_json: &Value,
provider_api_format: &str,
client_api_format: &str,
report_context: Option<&Value>,
) -> Result<Option<SyncToStreamBridgeOutcome>, GatewayError> {
let provider_api_format = normalize_api_format(provider_api_format);
let client_api_format = normalize_api_format(client_api_format);
if provider_api_format == "openai:image" && client_api_format == "openai:image" {
return maybe_bridge_openai_image_sync_json_to_stream(provider_body_json, report_context);
}
if !is_standard_api_format(provider_api_format.as_str())
|| !is_standard_api_format(client_api_format.as_str())
{
return Ok(None);
}
let bridge_context = build_bridge_report_context(
report_context,
provider_api_format.as_str(),
client_api_format.as_str(),
);
let Some(openai_responses_response) = convert_provider_sync_response_to_openai_responses(
provider_body_json,
provider_api_format.as_str(),
&bridge_context,
) else {
return Ok(None);
};
let terminal_summary =
build_terminal_summary_from_openai_responses_response(&openai_responses_response);
let canonical_frames = build_canonical_frames_from_openai_responses_response(
&openai_responses_response,
&bridge_context,
)?;
let sse_body =
emit_client_stream_from_canonical_frames(canonical_frames, client_api_format.as_str())?;
Ok(Some(SyncToStreamBridgeOutcome {
sse_body,
terminal_summary,
}))
}
fn maybe_bridge_openai_image_sync_json_to_stream(
provider_body_json: &Value,
report_context: Option<&Value>,
) -> Result<Option<SyncToStreamBridgeOutcome>, GatewayError> {
let Some(response) = provider_body_json.as_object() else {
return Ok(None);
};
let Some(image) = response
.get("data")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_object)
.find_map(extract_openai_image_sync_b64_json)
else {
return Ok(None);
};
let usage = response.get("usage").cloned().unwrap_or(Value::Null);
let event_name = openai_image_completed_event_name(report_context);
let sse_body = encode_json_sse(
Some(event_name),
&json!({
"type": event_name,
"b64_json": image,
"usage": usage,
}),
)?;
Ok(Some(SyncToStreamBridgeOutcome {
sse_body,
terminal_summary: Some(ExecutionStreamTerminalSummary {
standardized_usage: response
.get("usage")
.and_then(standardized_usage_from_openai_usage),
finish_reason: Some("stop".to_string()),
response_id: response
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned),
model: response
.get("model")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| image_bridge_model(report_context)),
observed_finish: true,
unknown_event_count: 0,
parser_error: None,
}),
}))
}
fn normalize_api_format(value: &str) -> String {
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn is_standard_api_format(value: &str) -> bool {
matches!(
value,
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:messages"
| "gemini:generate_content"
)
}
fn extract_openai_image_sync_b64_json(item: &serde_json::Map<String, Value>) -> Option<String> {
item.get("b64_json")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
item.get("url")
.and_then(Value::as_str)
.and_then(extract_base64_from_data_url)
})
}
fn extract_base64_from_data_url(value: &str) -> Option<String> {
let trimmed = value.trim();
let (metadata, payload) = trimmed.split_once(',')?;
if !metadata.starts_with("data:") || !metadata.ends_with(";base64") {
return None;
}
(!payload.trim().is_empty()).then(|| payload.trim().to_string())
}
fn openai_image_completed_event_name(report_context: Option<&Value>) -> &'static str {
if openai_image_request_operation(report_context) == Some("edit") {
"image_edit.completed"
} else {
"image_generation.completed"
}
}
fn openai_image_request_operation(report_context: Option<&Value>) -> Option<&str> {
report_context
.and_then(|value| value.get("image_request"))
.and_then(|value| value.get("operation"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn image_bridge_model(report_context: Option<&Value>) -> Option<String> {
report_context.and_then(|context| {
context
.get("mapped_model")
.or_else(|| context.get("model"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
}
fn build_bridge_report_context(
report_context: Option<&Value>,
provider_api_format: &str,
client_api_format: &str,
) -> Value {
let mut context = report_context
.cloned()
.filter(Value::is_object)
.unwrap_or_else(|| json!({}));
let object = context
.as_object_mut()
.expect("bridge report context should stay object");
object
.entry("provider_api_format".to_string())
.or_insert_with(|| Value::String(provider_api_format.to_string()));
object
.entry("client_api_format".to_string())
.or_insert_with(|| Value::String(client_api_format.to_string()));
context
}
fn convert_provider_sync_response_to_openai_responses(
provider_body_json: &Value,
provider_api_format: &str,
report_context: &Value,
) -> Option<Value> {
match provider_api_format {
"openai:responses" | "openai:responses:compact" => Some(provider_body_json.clone()),
"openai:chat" => convert_openai_chat_response_to_openai_responses(
provider_body_json,
report_context,
false,
),
"claude:messages" => {
convert_claude_response_to_openai_responses(provider_body_json, report_context)
}
"gemini:generate_content" => {
convert_gemini_response_to_openai_responses(provider_body_json, report_context)
}
_ => None,
}
}
fn build_canonical_frames_from_openai_responses_response(
openai_responses_response: &Value,
report_context: &Value,
) -> Result<Vec<crate::ai_pipeline::CanonicalStreamFrame>, GatewayError> {
let mut state = OpenAIResponsesProviderState::default();
let line = format!(
"data: {}\n",
serde_json::to_string(&json!({
"type": "response.completed",
"response": openai_responses_response,
}))
.map_err(|err| GatewayError::Internal(err.to_string()))?
);
let mut frames = state
.push_line(report_context, line.into_bytes())
.map_err(|err| GatewayError::Internal(err.to_string()))?;
frames.extend(
state
.finish(report_context)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
Ok(frames)
}
fn emit_client_stream_from_canonical_frames(
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
client_api_format: &str,
) -> Result<Vec<u8>, GatewayError> {
match client_api_format {
"openai:chat" => {
let mut emitter = OpenAIChatClientEmitter::default();
emit_with_openai_chat_emitter(&mut emitter, canonical_frames)
}
"openai:responses" | "openai:responses:compact" => {
let mut emitter = OpenAIResponsesClientEmitter::default();
emit_with_openai_responses_emitter(&mut emitter, canonical_frames)
}
"claude:messages" => {
let mut emitter = ClaudeClientEmitter::default();
emit_with_claude_emitter(&mut emitter, canonical_frames)
}
"gemini:generate_content" => {
let mut emitter = GeminiClientEmitter::default();
emit_with_gemini_emitter(&mut emitter, canonical_frames)
}
_ => Ok(Vec::new()),
}
}
fn emit_with_openai_chat_emitter(
emitter: &mut OpenAIChatClientEmitter,
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
for frame in canonical_frames {
output.extend(
emitter
.emit(frame)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(
emitter
.finish()
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
Ok(output)
}
fn emit_with_openai_responses_emitter(
emitter: &mut OpenAIResponsesClientEmitter,
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
for frame in canonical_frames {
output.extend(
emitter
.emit(frame)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(
emitter
.finish()
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
Ok(output)
}
fn emit_with_claude_emitter(
emitter: &mut ClaudeClientEmitter,
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
for frame in canonical_frames {
output.extend(
emitter
.emit(frame)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(
emitter
.finish()
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
Ok(output)
}
fn emit_with_gemini_emitter(
emitter: &mut GeminiClientEmitter,
canonical_frames: Vec<crate::ai_pipeline::CanonicalStreamFrame>,
) -> Result<Vec<u8>, GatewayError> {
let mut output = Vec::new();
for frame in canonical_frames {
output.extend(
emitter
.emit(frame)
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
}
output.extend(
emitter
.finish()
.map_err(|err| GatewayError::Internal(err.to_string()))?,
);
Ok(output)
}
fn build_terminal_summary_from_openai_responses_response(
openai_responses_response: &Value,
) -> Option<ExecutionStreamTerminalSummary> {
let response = openai_responses_response.as_object()?;
let response_id = response
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned);
let model = response
.get("model")
.and_then(Value::as_str)
.map(ToOwned::to_owned);
let finish_reason = response
.get("output")
.and_then(Value::as_array)
.map(|output| resolve_openai_responses_finish_reason(output))
.filter(|value| !value.trim().is_empty());
let standardized_usage = response
.get("usage")
.and_then(standardized_usage_from_openai_usage);
Some(ExecutionStreamTerminalSummary {
standardized_usage,
finish_reason,
response_id,
model,
observed_finish: true,
unknown_event_count: 0,
parser_error: None,
})
}
fn resolve_openai_responses_finish_reason(output: &[Value]) -> String {
let has_tool_calls = output.iter().filter_map(Value::as_object).any(|item| {
item.get("type")
.and_then(Value::as_str)
.is_some_and(|value| value == "function_call")
});
if has_tool_calls {
"tool_calls".to_string()
} else {
"stop".to_string()
}
}
fn standardized_usage_from_openai_usage(value: &Value) -> Option<StandardizedUsage> {
let usage = value.as_object()?;
let mut input_tokens = usage
.get("input_tokens")
.or_else(|| usage.get("prompt_tokens"))
.and_then(Value::as_i64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.or_else(|| usage.get("completion_tokens"))
.and_then(Value::as_i64)
.unwrap_or(0);
let cache_creation_tokens = usage
.get("cache_creation_input_tokens")
.and_then(Value::as_i64)
.or_else(|| {
usage
.get("input_tokens_details")
.or_else(|| usage.get("prompt_tokens_details"))
.and_then(Value::as_object)
.and_then(|details| details.get("cached_creation_tokens"))
.and_then(Value::as_i64)
})
.unwrap_or(0);
let cache_read_tokens = usage
.get("cache_read_input_tokens")
.and_then(Value::as_i64)
.or_else(|| {
usage
.get("input_tokens_details")
.or_else(|| usage.get("prompt_tokens_details"))
.and_then(Value::as_object)
.and_then(|details| details.get("cached_tokens"))
.and_then(Value::as_i64)
})
.unwrap_or(0);
let total_tokens = usage.get("total_tokens").and_then(Value::as_i64).unwrap_or(
input_tokens
.saturating_add(output_tokens)
.saturating_add(cache_creation_tokens)
.saturating_add(cache_read_tokens),
);
if input_tokens == 0 && total_tokens > output_tokens {
input_tokens = total_tokens.saturating_sub(output_tokens);
}
let mut standardized_usage = StandardizedUsage::new();
standardized_usage.input_tokens = input_tokens;
standardized_usage.output_tokens = output_tokens;
standardized_usage.cache_creation_tokens = cache_creation_tokens;
standardized_usage.cache_read_tokens = cache_read_tokens;
standardized_usage
.dimensions
.insert("total_tokens".to_string(), json!(total_tokens));
Some(standardized_usage.normalize_cache_creation_breakdown())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{maybe_bridge_standard_sync_json_to_stream, standardized_usage_from_openai_usage};
fn utf8(bytes: Vec<u8>) -> String {
String::from_utf8(bytes).expect("utf8 should decode")
}
#[test]
fn openai_sync_usage_derives_missing_input_tokens_from_total() {
let usage = standardized_usage_from_openai_usage(&json!({
"output_tokens": 177,
"total_tokens": 20_612,
"input_tokens_details": {
"cached_tokens": 19_840,
},
}))
.expect("usage should parse");
assert_eq!(usage.input_tokens, 20_435);
assert_eq!(usage.output_tokens, 177);
assert_eq!(usage.cache_read_tokens, 19_840);
}
#[test]
fn bridges_openai_image_sync_json_to_generation_completed_sse() {
let report_context = json!({
"provider_api_format": "openai:image",
"client_api_format": "openai:image",
"mapped_model": "gpt-image-1",
"image_request": {
"operation": "generate"
}
});
let outcome = maybe_bridge_standard_sync_json_to_stream(
&json!({
"created": 1776971267,
"data": [{
"b64_json": "aGVsbG8="
}],
"usage": {
"total_tokens": 100,
"input_tokens": 50,
"output_tokens": 50,
"input_tokens_details": {
"text_tokens": 10,
"image_tokens": 40
}
}
}),
"openai:image",
"openai:image",
Some(&report_context),
)
.expect("bridge should succeed")
.expect("bridge should produce sse");
let output = utf8(outcome.sse_body);
assert!(output.contains("event: image_generation.completed"));
assert!(output.contains("\"type\":\"image_generation.completed\""));
assert!(output.contains("\"b64_json\":\"aGVsbG8=\""));
assert!(output.contains("\"total_tokens\":100"));
let summary = outcome
.terminal_summary
.expect("terminal summary should exist");
assert_eq!(summary.model.as_deref(), Some("gpt-image-1"));
assert_eq!(summary.finish_reason.as_deref(), Some("stop"));
assert_eq!(
summary
.standardized_usage
.as_ref()
.and_then(|usage| usage.dimensions.get("total_tokens"))
.cloned(),
Some(json!(100))
);
}
#[test]
fn bridges_openai_image_sync_data_url_to_edit_completed_sse() {
let report_context = json!({
"provider_api_format": "openai:image",
"client_api_format": "openai:image",
"image_request": {
"operation": "edit"
}
});
let outcome = maybe_bridge_standard_sync_json_to_stream(
&json!({
"created": 1776971267,
"data": [{
"url": "data:image/webp;base64,d29ybGQ="
}],
"usage": {
"total_tokens": 9,
"input_tokens": 4,
"output_tokens": 5
}
}),
"openai:image",
"openai:image",
Some(&report_context),
)
.expect("bridge should succeed")
.expect("bridge should produce sse");
let output = utf8(outcome.sse_body);
assert!(output.contains("event: image_edit.completed"));
assert!(output.contains("\"type\":\"image_edit.completed\""));
assert!(output.contains("\"b64_json\":\"d29ybGQ=\""));
assert!(output.contains("\"total_tokens\":9"));
}
}

View File

@@ -1,6 +0,0 @@
//! Standard finalize streaming conversion helpers.
pub(crate) use crate::ai_pipeline::CanonicalStreamFrame;
mod orchestrator;
pub(crate) use orchestrator::StreamingStandardConversionState;

View File

@@ -1,49 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::adaptation::private_envelope::transform_provider_private_stream_line as transform_envelope_line;
use crate::ai_pipeline::{
provider_adaptation_should_unwrap_stream_envelope, StreamingStandardFormatMatrix,
};
use crate::GatewayError;
#[derive(Default)]
pub(crate) struct StreamingStandardConversionState {
matrix: StreamingStandardFormatMatrix,
}
impl StreamingStandardConversionState {
pub(crate) fn transform_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<u8>, GatewayError> {
let line = if should_unwrap_envelope(report_context) {
transform_envelope_line(report_context, line)
.map_err(|err| GatewayError::Internal(err.to_string()))?
} else {
line
};
if line.is_empty() {
return Ok(Vec::new());
}
self.matrix
.transform_line(report_context, line)
.map_err(Into::into)
}
pub(crate) fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, GatewayError> {
self.matrix.finish(report_context).map_err(Into::into)
}
}
fn should_unwrap_envelope(report_context: &Value) -> bool {
let envelope_name = report_context
.get("envelope_name")
.and_then(Value::as_str)
.unwrap_or_default();
let provider_api_format = report_context
.get("provider_api_format")
.and_then(Value::as_str)
.unwrap_or_default();
provider_adaptation_should_unwrap_stream_envelope(envelope_name, provider_api_format)
}

View File

@@ -1,622 +0,0 @@
use aether_contracts::ProxySnapshot;
use aether_scheduler_core::{SchedulerMinimalCandidateSelectionCandidate, SchedulerRankingOutcome};
use serde_json::{json, Map, Value};
use crate::ai_pipeline::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_pipeline::planner::passthrough::resolve_same_format_provider_transport_unsupported_reason_for_trace;
use crate::ai_pipeline::transport::{
body_rules_are_locally_supported, header_rules_are_locally_supported,
local_gemini_transport_unsupported_reason_with_network,
local_openai_chat_transport_unsupported_reason,
local_standard_transport_unsupported_reason_with_network, resolve_transport_tls_profile,
supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported,
};
use crate::ai_pipeline::{
request_conversion_enabled_for_transport, request_conversion_kind,
request_conversion_requires_enable_flag, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport, GatewayProviderTransportSnapshot,
};
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy};
use crate::append_execution_contract_fields_to_value;
pub(crate) struct LocalExecutionCandidateMetadataParts<'a> {
pub(crate) eligible: &'a EligibleLocalExecutionCandidate,
pub(crate) provider_api_format: &'a str,
pub(crate) client_api_format: &'a str,
pub(crate) extra_fields: Map<String, Value>,
}
pub(crate) fn append_ranking_metadata_to_object(
object: &mut Map<String, Value>,
ranking: &SchedulerRankingOutcome,
) {
object.insert(
"ranking_mode".to_string(),
Value::String(format!("{:?}", ranking.ranking_mode)),
);
object.insert(
"priority_mode".to_string(),
Value::String(format!("{:?}", ranking.priority_mode)),
);
object.insert(
"ranking_index".to_string(),
Value::Number(serde_json::Number::from(ranking.ranking_index as u64)),
);
object.insert(
"priority_slot".to_string(),
Value::Number(serde_json::Number::from(i64::from(ranking.priority_slot))),
);
if let Some(promoted_by) = ranking.promoted_by {
object.insert(
"promoted_by".to_string(),
Value::String(promoted_by.to_string()),
);
}
if let Some(demoted_by) = ranking.demoted_by {
object.insert(
"demoted_by".to_string(),
Value::String(demoted_by.to_string()),
);
}
}
pub(crate) fn build_request_trace_proxy_value(
transport: Option<&GatewayProviderTransportSnapshot>,
resolved_proxy: Option<&ProxySnapshot>,
) -> Option<Value> {
let resolved_proxy = resolved_proxy?;
let mut object = Map::new();
if let Some(node_id) = trimmed_non_empty(resolved_proxy.node_id.as_deref()) {
object.insert("node_id".to_string(), Value::String(node_id));
}
if let Some(node_name) = trimmed_non_empty(resolved_proxy.label.as_deref()) {
object.insert("node_name".to_string(), Value::String(node_name));
}
if let Some(url) = sanitize_trace_proxy_url(resolved_proxy.url.as_deref()) {
object.insert("url".to_string(), Value::String(url));
}
if let Some(source) = resolve_request_trace_proxy_source(transport, true) {
object.insert("source".to_string(), Value::String(source.to_string()));
}
(!object.is_empty()).then_some(Value::Object(object))
}
pub(crate) fn build_local_execution_candidate_metadata(
parts: LocalExecutionCandidateMetadataParts<'_>,
) -> Value {
build_local_execution_candidate_metadata_for_candidate(
&parts.eligible.candidate,
Some(parts.eligible.transport.as_ref()),
parts.provider_api_format,
parts.client_api_format,
parts.extra_fields,
)
}
pub(crate) fn build_local_execution_candidate_metadata_for_candidate(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: Option<&GatewayProviderTransportSnapshot>,
provider_api_format: &str,
client_api_format: &str,
extra_fields: Map<String, Value>,
) -> Value {
let mut object = Map::new();
object.insert(
"provider_api_format".to_string(),
Value::String(provider_api_format.to_string()),
);
object.insert(
"client_api_format".to_string(),
Value::String(client_api_format.to_string()),
);
object.insert(
"global_model_id".to_string(),
Value::String(candidate.global_model_id.clone()),
);
object.insert(
"global_model_name".to_string(),
Value::String(candidate.global_model_name.clone()),
);
object.insert(
"model_id".to_string(),
Value::String(candidate.model_id.clone()),
);
object.insert(
"selected_provider_model_name".to_string(),
Value::String(candidate.selected_provider_model_name.clone()),
);
object.insert(
"mapping_matched_model".to_string(),
candidate
.mapping_matched_model
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
);
object.insert(
"provider_name".to_string(),
Value::String(candidate.provider_name.clone()),
);
object.insert(
"key_name".to_string(),
Value::String(candidate.key_name.clone()),
);
object.extend(extra_fields);
append_transport_diagnostics_to_value(
Value::Object(object),
transport,
client_api_format,
provider_api_format,
)
}
pub(crate) fn build_local_execution_candidate_contract_metadata(
parts: LocalExecutionCandidateMetadataParts<'_>,
execution_strategy: ExecutionStrategy,
conversion_mode: ConversionMode,
provider_contract: &str,
) -> Value {
append_execution_contract_fields_to_value(
build_local_execution_candidate_metadata_for_candidate(
&parts.eligible.candidate,
Some(parts.eligible.transport.as_ref()),
parts.provider_api_format,
parts.client_api_format,
parts.extra_fields,
),
execution_strategy,
conversion_mode,
parts.client_api_format,
provider_contract,
)
}
pub(crate) fn build_local_execution_candidate_contract_metadata_for_candidate(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: Option<&GatewayProviderTransportSnapshot>,
provider_api_format: &str,
client_api_format: &str,
extra_fields: Map<String, Value>,
execution_strategy: ExecutionStrategy,
conversion_mode: ConversionMode,
provider_contract: &str,
) -> Value {
append_execution_contract_fields_to_value(
build_local_execution_candidate_metadata_for_candidate(
candidate,
transport,
provider_api_format,
client_api_format,
extra_fields,
),
execution_strategy,
conversion_mode,
client_api_format,
provider_contract,
)
}
fn append_transport_diagnostics_to_value(
value: Value,
transport: Option<&GatewayProviderTransportSnapshot>,
client_api_format: &str,
provider_api_format: &str,
) -> Value {
let Value::Object(mut object) = value else {
return value;
};
object.insert(
"transport_diagnostics".to_string(),
transport
.map(|transport| {
build_transport_diagnostics(transport, client_api_format, provider_api_format)
})
.unwrap_or_else(|| json!({ "transport_snapshot_available": false })),
);
Value::Object(object)
}
fn build_transport_diagnostics(
transport: &GatewayProviderTransportSnapshot,
client_api_format: &str,
provider_api_format: &str,
) -> Value {
let resolved_tls_profile = resolve_transport_tls_profile(transport);
let configured_tls_profile = transport
.key
.fingerprint
.as_ref()
.and_then(Value::as_object)
.and_then(|value| value.get("tls_profile"))
.cloned()
.unwrap_or(Value::Null);
let has_oauth_config = transport.key.decrypted_auth_config.is_some();
let oauth_resolution_supported =
!has_oauth_config || supports_local_oauth_request_auth_resolution(transport);
let request_transport_unsupported_reason = resolve_request_transport_unsupported_reason(
transport,
client_api_format,
provider_api_format,
);
json!({
"transport_snapshot_available": true,
"provider_type": transport.provider.provider_type,
"provider_is_active": transport.provider.is_active,
"endpoint_is_active": transport.endpoint.is_active,
"key_is_active": transport.key.is_active,
"provider_enable_format_conversion": transport.provider.enable_format_conversion,
"provider_keep_priority_on_conversion": transport.provider.keep_priority_on_conversion,
"endpoint_format_acceptance_config": transport.endpoint.format_acceptance_config,
"endpoint_custom_path": transport.endpoint.custom_path,
"header_rules": transport.endpoint.header_rules,
"header_rules_supported": header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()),
"body_rules": transport.endpoint.body_rules,
"body_rules_supported": body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref()),
"proxy": {
"locally_supported": transport_proxy_is_locally_supported(transport),
"provider": summarize_proxy_config(transport.provider.proxy.as_ref()),
"endpoint": summarize_proxy_config(transport.endpoint.proxy.as_ref()),
"key": summarize_proxy_config(transport.key.proxy.as_ref()),
},
"auth": {
"key_auth_type": transport.key.auth_type,
"has_oauth_config": has_oauth_config,
"oauth_request_auth_resolution_supported": oauth_resolution_supported,
},
"fingerprint": transport.key.fingerprint,
"configured_tls_profile": configured_tls_profile,
"resolved_tls_profile": resolved_tls_profile,
"request_pair": {
"client_api_format": client_api_format,
"provider_api_format": provider_api_format,
"requires_conversion_enable_flag": request_conversion_requires_enable_flag(
client_api_format,
provider_api_format,
),
"conversion_enabled": request_conversion_enabled_for_transport(
transport,
client_api_format,
provider_api_format,
),
"pair_allowed": request_pair_allowed_for_transport(
transport,
client_api_format,
provider_api_format,
),
"transport_unsupported_reason": request_transport_unsupported_reason,
},
})
}
fn summarize_proxy_config(proxy: Option<&Value>) -> Value {
let Some(object) = proxy.and_then(Value::as_object) else {
return Value::Null;
};
let has_url = object
.get("url")
.or_else(|| object.get("proxy_url"))
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty());
json!({
"enabled": object.get("enabled").cloned().unwrap_or(Value::Null),
"mode": object.get("mode").cloned().unwrap_or(Value::Null),
"node_id": object.get("node_id").cloned().unwrap_or(Value::Null),
"label": object.get("label").cloned().unwrap_or(Value::Null),
"has_url": has_url,
})
}
fn resolve_request_trace_proxy_source(
transport: Option<&GatewayProviderTransportSnapshot>,
has_resolved_proxy: bool,
) -> Option<&'static str> {
let transport = transport?;
if transport_has_explicit_proxy(transport.key.proxy.as_ref()) {
return Some("key");
}
if transport_has_explicit_proxy(transport.endpoint.proxy.as_ref()) {
return Some("endpoint");
}
if transport_has_explicit_proxy(transport.provider.proxy.as_ref()) {
return Some("provider");
}
has_resolved_proxy.then_some("system")
}
fn transport_has_explicit_proxy(proxy: Option<&Value>) -> bool {
let Some(object) = proxy.and_then(Value::as_object) else {
return false;
};
let enabled = object
.get("enabled")
.and_then(Value::as_bool)
.unwrap_or(true);
if !enabled {
return false;
}
object
.get("node_id")
.or_else(|| object.get("url"))
.or_else(|| object.get("proxy_url"))
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
}
fn sanitize_trace_proxy_url(url: Option<&str>) -> Option<String> {
let raw = url.map(str::trim).filter(|value| !value.is_empty())?;
let parsed = url::Url::parse(raw).ok()?;
let scheme = parsed.scheme().trim();
let host = parsed.host_str()?.trim();
if scheme.is_empty() || host.is_empty() {
return None;
}
let mut safe = format!("{scheme}://{host}");
if let Some(port) = parsed.port() {
safe.push(':');
safe.push_str(port.to_string().as_str());
}
Some(safe)
}
fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn resolve_request_transport_unsupported_reason(
transport: &GatewayProviderTransportSnapshot,
client_api_format: &str,
provider_api_format: &str,
) -> Option<&'static str> {
let client_api_format = client_api_format.trim().to_ascii_lowercase();
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
if client_api_format == provider_api_format {
if let Some(skip_reason) =
resolve_same_format_provider_transport_unsupported_reason_for_trace(
transport,
provider_api_format.as_str(),
)
{
return Some(skip_reason);
}
return match provider_api_format.as_str() {
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
"gemini:generate_content" => local_gemini_transport_unsupported_reason_with_network(
transport,
provider_api_format.as_str(),
),
_ => local_standard_transport_unsupported_reason_with_network(
transport,
provider_api_format.as_str(),
),
};
}
match request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str()) {
Some(kind) => request_conversion_transport_unsupported_reason(transport, kind),
None => Some("transport_api_format_unsupported"),
}
}
#[cfg(test)]
mod tests {
use super::{
build_local_execution_candidate_contract_metadata_for_candidate,
build_local_execution_candidate_metadata_for_candidate,
};
use crate::ai_pipeline::transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use serde_json::{json, Value};
fn sample_candidate() -> SchedulerMinimalCandidateSelectionCandidate {
SchedulerMinimalCandidateSelectionCandidate {
provider_id: "provider-1".to_string(),
provider_name: "RightCode".to_string(),
provider_type: "codex".to_string(),
provider_priority: 22,
endpoint_id: "endpoint-1".to_string(),
endpoint_api_format: "openai:responses".to_string(),
key_id: "key-1".to_string(),
key_name: "codex".to_string(),
key_auth_type: "oauth".to_string(),
key_internal_priority: 10,
key_global_priority_for_format: None,
key_capabilities: None,
model_id: "model-1".to_string(),
global_model_id: "global-1".to_string(),
global_model_name: "gpt-5.4".to_string(),
selected_provider_model_name: "gpt-5.4".to_string(),
mapping_matched_model: None,
}
}
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "RightCode".to_string(),
provider_type: "codex".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: Some(json!({"enabled": true, "mode": "node", "node_id": "proxy-node-1"})),
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "openai:responses".to_string(),
api_family: None,
endpoint_kind: None,
is_active: true,
base_url: "https://example.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: Some("/v1/responses".to_string()),
config: None,
format_acceptance_config: Some(json!({
"enabled": true,
"accept_formats": ["claude:messages"]
})),
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "codex".to_string(),
auth_type: "oauth".to_string(),
is_active: true,
api_formats: None,
auth_type_by_format: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: Some(json!({
"tls_profile": "chrome_136",
"user_agent": "Mozilla/5.0"
})),
decrypted_api_key: "sk-test".to_string(),
decrypted_auth_config: None,
},
}
}
fn sample_claude_code_transport_without_auth() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-cc-1".to_string(),
name: "NekoCode".to_string(),
provider_type: "claude_code".to_string(),
website: Some("https://nekocode.ai".to_string()),
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
base_url: "https://api.anthropic.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
name: "CC-特价-0.4".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
decrypted_api_key: "__placeholder__".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn candidate_contract_metadata_includes_transport_diagnostics() {
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
&sample_candidate(),
Some(&sample_transport()),
"openai:responses",
"claude:messages",
serde_json::Map::new(),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
"openai:responses",
);
assert_eq!(metadata["transport_diagnostics"]["provider_type"], "codex");
assert_eq!(
metadata["transport_diagnostics"]["fingerprint"]["tls_profile"],
"chrome_136"
);
assert_eq!(
metadata["transport_diagnostics"]["resolved_tls_profile"],
"chrome_136"
);
assert_eq!(
metadata["transport_diagnostics"]["request_pair"]["conversion_enabled"],
Value::Bool(true)
);
assert!(
metadata["transport_diagnostics"]["request_pair"]["transport_unsupported_reason"]
.is_null()
);
}
#[test]
fn candidate_metadata_marks_missing_transport_snapshot() {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
None,
"openai:responses",
"openai:responses",
serde_json::Map::new(),
);
assert_eq!(
metadata["transport_diagnostics"]["transport_snapshot_available"],
Value::Bool(false)
);
}
#[test]
fn candidate_metadata_uses_same_format_provider_specific_transport_reason() {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
Some(&sample_claude_code_transport_without_auth()),
"claude:messages",
"claude:messages",
serde_json::Map::new(),
);
assert_eq!(
metadata["transport_diagnostics"]["request_pair"]["transport_unsupported_reason"],
Value::String("transport_auth_unavailable".to_string())
);
}
}

View File

@@ -1,417 +0,0 @@
use std::sync::Arc;
use aether_provider_transport::provider_types::provider_type_is_fixed;
use tracing::warn;
use aether_scheduler_core::{SchedulerMinimalCandidateSelectionCandidate, SchedulerRankingOutcome};
use crate::ai_pipeline::{
GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot, PlannerAppState,
};
use crate::orchestration::LocalExecutionCandidateMetadata;
use super::candidate_ranking::rank_eligible_local_execution_candidates;
use super::pool_scheduler::apply_local_execution_pool_scheduler;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EligibleLocalExecutionCandidate {
pub(crate) candidate: SchedulerMinimalCandidateSelectionCandidate,
pub(crate) transport: Arc<GatewayProviderTransportSnapshot>,
pub(crate) provider_api_format: String,
pub(crate) orchestration: LocalExecutionCandidateMetadata,
pub(crate) ranking: Option<SchedulerRankingOutcome>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SkippedLocalExecutionCandidate {
pub(crate) candidate: SchedulerMinimalCandidateSelectionCandidate,
pub(crate) skip_reason: &'static str,
pub(crate) transport: Option<Arc<GatewayProviderTransportSnapshot>>,
pub(crate) ranking: Option<SchedulerRankingOutcome>,
pub(crate) extra_data: Option<serde_json::Value>,
}
impl SkippedLocalExecutionCandidate {
pub(crate) fn transport_ref(&self) -> Option<&GatewayProviderTransportSnapshot> {
self.transport.as_deref()
}
}
pub(crate) async fn resolve_and_rank_local_execution_candidates(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: &str,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let requested_model = requested_model.trim();
resolve_and_rank_local_execution_candidates_with_gate(
state,
candidates,
client_api_format,
Some(requested_model),
auth_snapshot,
required_capabilities,
sticky_session_token,
|candidate, transport, normalized_client_api_format| {
current_local_execution_candidate_skip_reason_with_transport(
candidate,
transport,
normalized_client_api_format,
requested_model,
)
},
)
.await
}
pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transport_pair_gate(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: Option<&str>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let requested_model = requested_model.map(str::trim);
resolve_and_rank_local_execution_candidates_with_gate(
state,
candidates,
client_api_format,
requested_model,
auth_snapshot,
required_capabilities,
sticky_session_token,
|candidate, transport, _normalized_client_api_format| {
current_local_execution_candidate_common_skip_reason_with_transport(
candidate,
transport,
requested_model,
)
},
)
.await
}
async fn resolve_and_rank_local_execution_candidates_with_gate<F>(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: Option<&str>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
runtime_skip_reason: F,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
)
where
F: Fn(
&SchedulerMinimalCandidateSelectionCandidate,
&GatewayProviderTransportSnapshot,
&str,
) -> Option<&'static str>,
{
let normalized_client_api_format = client_api_format.trim().to_ascii_lowercase();
let mut selectable = Vec::with_capacity(candidates.len());
let mut skipped = Vec::with_capacity(candidates.len());
for candidate in candidates {
let Some(transport) = read_candidate_transport_snapshot(state, &candidate).await else {
skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason: "transport_snapshot_missing",
transport: None,
ranking: None,
extra_data: None,
});
continue;
};
let transport = Arc::new(transport);
match runtime_skip_reason(
&candidate,
transport.as_ref(),
normalized_client_api_format.as_str(),
) {
Some(skip_reason) => skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason,
transport: Some(transport),
ranking: None,
extra_data: None,
}),
None => selectable.push(EligibleLocalExecutionCandidate {
provider_api_format: transport.endpoint.api_format.trim().to_ascii_lowercase(),
candidate,
transport,
orchestration: LocalExecutionCandidateMetadata::default(),
ranking: None,
}),
}
}
let ranked = rank_eligible_local_execution_candidates(
state,
selectable,
normalized_client_api_format.as_str(),
requested_model,
auth_snapshot,
required_capabilities,
)
.await;
let (ranked, pool_skipped) =
apply_local_execution_pool_scheduler(state, ranked, sticky_session_token).await;
skipped.extend(pool_skipped);
(ranked, skipped)
}
pub(crate) fn extract_pool_sticky_session_token(body_json: &serde_json::Value) -> Option<String> {
fn non_empty_str(value: Option<&serde_json::Value>) -> Option<&str> {
value
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
let object = body_json.as_object()?;
non_empty_str(object.get("prompt_cache_key"))
.or_else(|| non_empty_str(object.get("conversation_id")))
.or_else(|| non_empty_str(object.get("conversationId")))
.or_else(|| non_empty_str(object.get("session_id")))
.or_else(|| non_empty_str(object.get("sessionId")))
.or_else(|| {
object
.get("metadata")
.and_then(serde_json::Value::as_object)
.and_then(|metadata| {
non_empty_str(metadata.get("session_id"))
.or_else(|| non_empty_str(metadata.get("conversation_id")))
})
})
.or_else(|| {
object
.get("conversationState")
.and_then(serde_json::Value::as_object)
.and_then(|state| {
non_empty_str(state.get("conversationId"))
.or_else(|| non_empty_str(state.get("sessionId")))
})
})
.map(ToOwned::to_owned)
}
fn current_local_execution_candidate_common_skip_reason_with_transport(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: &GatewayProviderTransportSnapshot,
requested_model: Option<&str>,
) -> Option<&'static str> {
let requested_model = requested_model.unwrap_or_default();
if !transport.provider.is_active {
return Some("provider_inactive");
}
if !transport.endpoint.is_active {
return Some("endpoint_inactive");
}
if !transport.key.is_active {
return Some("key_inactive");
}
let endpoint_api_format = transport.endpoint.api_format.trim();
if !candidate
.endpoint_api_format
.trim()
.eq_ignore_ascii_case(endpoint_api_format)
&& !api_format_matches(&candidate.endpoint_api_format, endpoint_api_format)
{
return Some("endpoint_api_format_changed");
}
if !transport_key_supports_api_format(transport, endpoint_api_format) {
return Some("key_api_format_disabled");
}
if !transport_key_allows_candidate_model(transport, requested_model, candidate) {
return Some("key_model_disabled");
}
None
}
fn disabled_format_conversion_skip_reason(
transport: &GatewayProviderTransportSnapshot,
normalized_client_api_format: &str,
) -> Option<&'static str> {
let endpoint_api_format = transport.endpoint.api_format.trim();
if api_format_matches(endpoint_api_format, normalized_client_api_format) {
return None;
}
if crate::ai_pipeline::conversion::request_conversion_kind(
normalized_client_api_format,
endpoint_api_format,
)
.is_none()
{
return None;
}
if crate::ai_pipeline::conversion::request_conversion_requires_enable_flag(
normalized_client_api_format,
endpoint_api_format,
) && !crate::ai_pipeline::conversion::request_conversion_enabled_for_transport(
transport,
normalized_client_api_format,
endpoint_api_format,
) {
return Some("format_conversion_disabled");
}
None
}
fn current_local_execution_candidate_skip_reason_with_transport(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: &GatewayProviderTransportSnapshot,
normalized_client_api_format: &str,
requested_model: &str,
) -> Option<&'static str> {
if let Some(skip_reason) = current_local_execution_candidate_common_skip_reason_with_transport(
candidate,
transport,
Some(requested_model),
) {
return Some(skip_reason);
}
let endpoint_api_format = transport.endpoint.api_format.trim();
if api_format_matches(endpoint_api_format, normalized_client_api_format) {
return None;
}
if let Some(skip_reason) =
disabled_format_conversion_skip_reason(transport, normalized_client_api_format)
{
return Some(skip_reason);
}
if !crate::ai_pipeline::conversion::request_pair_allowed_for_transport(
transport,
normalized_client_api_format,
endpoint_api_format,
) {
return Some("transport_unsupported");
}
None
}
fn transport_key_supports_api_format(
transport: &GatewayProviderTransportSnapshot,
endpoint_api_format: &str,
) -> bool {
let provider_type = transport.provider.provider_type.trim();
let auth_type = transport.key.auth_type.trim();
let inherits_provider_api_formats = provider_type_is_fixed(provider_type)
&& (auth_type.eq_ignore_ascii_case("oauth")
|| (provider_type.eq_ignore_ascii_case("kiro")
&& auth_type.eq_ignore_ascii_case("bearer")
&& transport
.key
.decrypted_auth_config
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty())));
if inherits_provider_api_formats {
return true;
}
match transport.key.api_formats.as_deref() {
None => true,
Some(formats) => formats
.iter()
.any(|value| api_format_matches(value, endpoint_api_format)),
}
}
fn normalize_api_format_alias(value: &str) -> String {
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn transport_key_allows_candidate_model(
transport: &GatewayProviderTransportSnapshot,
requested_model: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> bool {
let Some(allowed_models) = transport.key.allowed_models.as_deref() else {
return true;
};
let requested_model = requested_model.trim();
let global_model_name = candidate.global_model_name.trim();
let selected_provider_model_name = candidate.selected_provider_model_name.trim();
let mapping_matched_model = candidate
.mapping_matched_model
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
for allowed_model in allowed_models.iter().map(String::as_str).map(str::trim) {
if allowed_model.is_empty() {
continue;
}
if allowed_model == requested_model
|| allowed_model == global_model_name
|| allowed_model == selected_provider_model_name
|| mapping_matched_model.is_some_and(|value| value == allowed_model)
{
return true;
}
}
false
}
pub(crate) async fn read_candidate_transport_snapshot(
state: PlannerAppState<'_>,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> Option<GatewayProviderTransportSnapshot> {
match state
.read_provider_transport_snapshot(
&candidate.provider_id,
&candidate.endpoint_id,
&candidate.key_id,
)
.await
{
Ok(Some(transport)) => Some(transport),
Ok(None) => None,
Err(error) => {
warn!(
event_name = "candidate_resolution_transport_load_failed",
log_type = "event",
provider_id = %candidate.provider_id,
endpoint_id = %candidate.endpoint_id,
key_id = %candidate.key_id,
error = ?error,
"failed to load provider transport while evaluating local candidate eligibility"
);
None
}
}
}

View File

@@ -1,34 +0,0 @@
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use crate::ai_pipeline::GatewayAuthApiKeySnapshot;
pub(crate) fn auth_snapshot_allows_cross_format_candidate(
auth_snapshot: &GatewayAuthApiKeySnapshot,
requested_model: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> bool {
if let Some(allowed_providers) = auth_snapshot.effective_allowed_providers() {
let provider_allowed = allowed_providers.iter().any(|value| {
aether_scheduler_core::provider_matches_allowed_value(
value,
&candidate.provider_id,
&candidate.provider_name,
&candidate.provider_type,
)
});
if !provider_allowed {
return false;
}
}
if let Some(allowed_models) = auth_snapshot.effective_allowed_models() {
let model_allowed = allowed_models
.iter()
.any(|value| value == requested_model || value == &candidate.global_model_name);
if !model_allowed {
return false;
}
}
true
}

View File

@@ -1,260 +0,0 @@
use axum::body::Bytes;
pub(crate) use crate::ai_pipeline::contracts::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::{
extract_gemini_model_from_path as extract_gemini_model_from_path_impl,
force_upstream_streaming_for_provider as force_upstream_streaming_for_provider_impl,
is_json_request, parse_direct_request_body as parse_direct_request_body_impl,
};
use crate::LocalExecutionRuntimeMissDiagnostic;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestedModelFamily {
Standard,
Gemini,
}
pub(crate) fn parse_direct_request_body(
parts: &http::request::Parts,
body_bytes: &Bytes,
) -> Option<(serde_json::Value, Option<String>)> {
parse_direct_request_body_impl(is_json_request(&parts.headers), body_bytes.as_ref())
}
pub(crate) fn force_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
force_upstream_streaming_for_provider_impl(provider_type, provider_api_format)
}
pub(crate) fn extract_standard_requested_model(body_json: &serde_json::Value) -> Option<String> {
body_json
.get("model")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
pub(crate) fn extract_requested_model_from_request(
parts: &http::request::Parts,
body_json: &serde_json::Value,
family: RequestedModelFamily,
) -> Option<String> {
match family {
RequestedModelFamily::Standard => extract_standard_requested_model(body_json),
RequestedModelFamily::Gemini => extract_gemini_model_from_path_impl(parts.uri.path()),
}
}
pub(crate) fn build_local_runtime_miss_diagnostic(
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
reason: &str,
) -> LocalExecutionRuntimeMissDiagnostic {
LocalExecutionRuntimeMissDiagnostic {
reason: reason.to_string(),
route_family: decision.route_family.clone(),
route_kind: decision.route_kind.clone(),
public_path: Some(decision.public_path.clone()),
plan_kind: Some(plan_kind.to_string()),
requested_model: requested_model.map(ToOwned::to_owned),
candidate_count: None,
skipped_candidate_count: None,
skip_reasons: std::collections::BTreeMap::new(),
}
}
pub(crate) fn apply_local_candidate_evaluation_progress(
diagnostic: &mut LocalExecutionRuntimeMissDiagnostic,
candidate_count: usize,
) {
diagnostic.candidate_count = Some(candidate_count);
diagnostic.reason = if candidate_count == 0 {
"candidate_list_empty".to_string()
} else {
"candidate_evaluation_incomplete".to_string()
};
}
pub(crate) fn apply_local_candidate_terminal_plan_reason(
diagnostic: &mut LocalExecutionRuntimeMissDiagnostic,
no_plan_reason: &'static str,
) {
let candidate_count = diagnostic.candidate_count.unwrap_or(0);
let skipped_candidate_count = diagnostic.skipped_candidate_count.unwrap_or(0);
diagnostic.reason = if candidate_count == 0 {
"candidate_list_empty".to_string()
} else if skipped_candidate_count >= candidate_count
&& diagnostic.skip_reasons.len() == 1
&& diagnostic
.skip_reasons
.get("api_key_concurrency_limit_reached")
.copied()
.unwrap_or(0)
> 0
{
"api_key_concurrency_limit_reached".to_string()
} else if skipped_candidate_count >= candidate_count {
"all_candidates_skipped".to_string()
} else {
no_plan_reason.to_string()
};
}
#[cfg(test)]
mod tests {
use super::{
apply_local_candidate_evaluation_progress, apply_local_candidate_terminal_plan_reason,
build_local_runtime_miss_diagnostic, extract_requested_model_from_request,
extract_standard_requested_model, force_upstream_streaming_for_provider,
RequestedModelFamily,
};
use axum::http::Request;
use serde_json::json;
#[test]
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[test]
fn does_not_force_streaming_for_compact_or_other_provider_types() {
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"openai",
"openai:responses"
));
}
#[test]
fn extracts_standard_requested_model_from_request_body() {
let requested_model =
extract_standard_requested_model(&json!({ "model": " claude-sonnet-4 " }));
assert_eq!(requested_model.as_deref(), Some("claude-sonnet-4"));
}
#[test]
fn request_family_helper_delegates_standard_model_extraction() {
let request = Request::builder()
.uri("https://example.test/v1/chat/completions")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let requested_model = extract_requested_model_from_request(
&parts,
&json!({ "model": " claude-sonnet-4 " }),
RequestedModelFamily::Standard,
);
assert_eq!(requested_model.as_deref(), Some("claude-sonnet-4"));
}
#[test]
fn extracts_gemini_requested_model_from_request_path() {
let request = Request::builder()
.uri("https://example.test/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let requested_model =
extract_requested_model_from_request(&parts, &json!({}), RequestedModelFamily::Gemini);
assert_eq!(requested_model.as_deref(), Some("gemini-2.5-pro"));
}
#[test]
fn candidate_evaluation_progress_sets_candidate_count_and_reason() {
let mut diagnostic = build_local_runtime_miss_diagnostic(
&crate::ai_pipeline::GatewayControlDecision::synthetic(
"/v1/test",
Some("passthrough".to_string()),
Some("ai".to_string()),
Some("chat".to_string()),
Some("test#sync".to_string()),
),
"test_plan",
Some("test-model"),
"seed",
);
apply_local_candidate_evaluation_progress(&mut diagnostic, 0);
assert_eq!(diagnostic.candidate_count, Some(0));
assert_eq!(diagnostic.reason, "candidate_list_empty");
apply_local_candidate_evaluation_progress(&mut diagnostic, 3);
assert_eq!(diagnostic.candidate_count, Some(3));
assert_eq!(diagnostic.reason, "candidate_evaluation_incomplete");
}
#[test]
fn candidate_terminal_reason_prefers_empty_then_skipped_then_fallback() {
let mut diagnostic = build_local_runtime_miss_diagnostic(
&crate::ai_pipeline::GatewayControlDecision::synthetic(
"/v1/test",
Some("passthrough".to_string()),
Some("ai".to_string()),
Some("chat".to_string()),
Some("test#sync".to_string()),
),
"test_plan",
Some("test-model"),
"seed",
);
apply_local_candidate_terminal_plan_reason(&mut diagnostic, "no_local_sync_plans");
assert_eq!(diagnostic.reason, "candidate_list_empty");
diagnostic.candidate_count = Some(2);
diagnostic.skipped_candidate_count = Some(2);
apply_local_candidate_terminal_plan_reason(&mut diagnostic, "no_local_sync_plans");
assert_eq!(diagnostic.reason, "all_candidates_skipped");
diagnostic.skip_reasons = std::collections::BTreeMap::from([(
"api_key_concurrency_limit_reached".to_string(),
2,
)]);
apply_local_candidate_terminal_plan_reason(&mut diagnostic, "no_local_sync_plans");
assert_eq!(diagnostic.reason, "api_key_concurrency_limit_reached");
diagnostic.skipped_candidate_count = Some(1);
diagnostic.skip_reasons.clear();
apply_local_candidate_terminal_plan_reason(&mut diagnostic, "no_local_sync_plans");
assert_eq!(diagnostic.reason, "no_local_sync_plans");
}
}

View File

@@ -1,174 +0,0 @@
use std::collections::BTreeMap;
use crate::ai_pipeline::planner::common::{
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, OPENAI_VIDEO_CONTENT_PLAN_KIND,
};
use crate::ai_pipeline::planner::route::{
is_matching_stream_request, resolve_execution_runtime_stream_plan_kind,
};
use crate::ai_pipeline::{
resolve_decision_execution_runtime_auth_context, ConversionMode, ExecutionStrategy,
GatewayControlDecision,
};
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) async fn maybe_build_stream_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
let Some(plan_kind) = resolve_execution_runtime_stream_plan_kind(parts, decision) else {
return Ok(None);
};
if !is_matching_stream_request(plan_kind, parts, body_json, body_base64) {
return Ok(None);
}
if let Some(payload) = maybe_build_local_video_task_content_stream_decision_payload(
state, parts, trace_id, decision, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_image_decision_payload(
state,
parts,
body_json,
body_base64,
trace_id,
decision,
plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_openai_responses_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_standard_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_same_format_provider_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_stream_local_gemini_files_decision_payload(
state, parts, trace_id, decision, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
Ok(None)
}
async fn maybe_build_local_video_task_content_stream_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
if plan_kind != OPENAI_VIDEO_CONTENT_PLAN_KIND
|| decision.route_family.as_deref() != Some("openai")
{
return Ok(None);
}
let _ = state
.hydrate_video_task_for_route(decision.route_family.as_deref(), parts.uri.path())
.await?;
let Some(action) = state.video_tasks.prepare_openai_content_stream_action(
parts.uri.path(),
parts.uri.query(),
trace_id,
) else {
return Ok(None);
};
let crate::video_tasks::LocalVideoTaskContentAction::StreamPlan(plan) = action else {
return Ok(None);
};
let plan = *plan;
let provider_contract = plan.provider_api_format.clone();
let client_contract = plan.client_api_format.clone();
let execution_strategy = if plan.provider_api_format == plan.client_api_format {
ExecutionStrategy::LocalSameFormat
} else {
ExecutionStrategy::LocalCrossFormat
};
let conversion_mode = if plan.provider_api_format == plan.client_api_format {
ConversionMode::None
} else {
ConversionMode::Bidirectional
};
Ok(Some(GatewayControlSyncDecisionResponse {
action: EXECUTION_RUNTIME_STREAM_DECISION_ACTION.to_string(),
decision_kind: Some(plan_kind.to_string()),
execution_strategy: Some(execution_strategy.as_str().to_string()),
conversion_mode: Some(conversion_mode.as_str().to_string()),
request_id: Some(plan.request_id),
candidate_id: plan.candidate_id,
provider_name: plan.provider_name,
provider_id: Some(plan.provider_id),
endpoint_id: Some(plan.endpoint_id),
key_id: Some(plan.key_id),
upstream_base_url: None,
upstream_url: Some(plan.url),
provider_request_method: Some(plan.method),
auth_header: None,
auth_value: None,
provider_api_format: Some(plan.provider_api_format),
client_api_format: Some(plan.client_api_format),
provider_contract: Some(provider_contract),
client_contract: Some(client_contract),
model_name: plan.model_name,
mapped_model: None,
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: plan.headers,
provider_request_body: None,
provider_request_body_base64: None,
content_type: plan.content_type,
proxy: plan.proxy,
tls_profile: plan.tls_profile,
timeouts: plan.timeouts,
upstream_is_stream: true,
report_kind: None,
report_context: None,
auth_context: resolve_decision_execution_runtime_auth_context(decision),
}))
}

View File

@@ -1,366 +0,0 @@
use std::collections::BTreeMap;
use tracing::debug;
use url::Url;
use crate::ai_pipeline::planner::common::{
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
use crate::ai_pipeline::planner::route::resolve_execution_runtime_sync_plan_kind;
use crate::ai_pipeline::{
build_execution_runtime_auth_context, resolve_execution_runtime_auth_context, ConversionMode,
ExecutionStrategy, GatewayControlDecision,
};
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) async fn maybe_build_sync_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
body_is_empty: bool,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
let Some(plan_kind) = resolve_execution_runtime_sync_plan_kind(parts, decision) else {
return Ok(None);
};
if let Some(payload) = maybe_build_local_video_task_follow_up_sync_decision_payload(
state, parts, body_json, trace_id, decision, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_video_decision_payload(
state, parts, body_json, trace_id, decision, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_image_decision_payload(
state,
parts,
body_json,
body_base64,
trace_id,
decision,
plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_openai_responses_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_standard_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if let Some(payload) = super::maybe_build_sync_local_same_format_provider_decision_payload(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(Some(payload));
}
if matches!(
plan_kind,
GEMINI_FILES_LIST_PLAN_KIND | GEMINI_FILES_GET_PLAN_KIND | GEMINI_FILES_DELETE_PLAN_KIND
) {
if let Some(payload) = super::maybe_build_sync_local_gemini_files_decision_payload(
state,
parts,
body_json,
body_base64,
body_is_empty,
trace_id,
decision,
plan_kind,
)
.await?
{
return Ok(Some(payload));
}
}
Ok(None)
}
async fn maybe_build_local_video_task_follow_up_sync_decision_payload(
state: &AppState,
parts: &http::request::Parts,
body_json: &serde_json::Value,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
if !matches!(
plan_kind,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
| OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND
| OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND
| GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND
) {
return Ok(None);
}
let _ = state
.hydrate_video_task_for_route(decision.route_family.as_deref(), parts.uri.path())
.await?;
let auth_context = resolve_execution_runtime_auth_context(
state,
decision,
&parts.headers,
&parts.uri,
trace_id,
)
.await?;
let Some(auth_context) = auth_context else {
return Ok(None);
};
let Some(follow_up) = state.video_tasks.prepare_follow_up_sync_plan(
plan_kind,
parts.uri.path(),
Some(body_json),
Some(&auth_context),
trace_id,
) else {
return Ok(None);
};
let aether_video_tasks_core::LocalVideoTaskFollowUpPlan {
plan,
report_kind,
report_context,
} = follow_up;
let aether_contracts::ExecutionPlan {
request_id: _request_id,
candidate_id,
provider_name,
provider_id,
endpoint_id,
key_id,
method,
url,
headers,
content_type,
content_encoding: _content_encoding,
body,
stream: _stream,
client_api_format,
provider_api_format,
model_name,
proxy,
tls_profile,
timeouts,
} = plan;
let auth_pair = extract_auth_header_pair(&headers);
let execution_strategy = if provider_api_format == client_api_format {
ExecutionStrategy::LocalSameFormat
} else {
ExecutionStrategy::LocalCrossFormat
};
let conversion_mode = if provider_api_format == client_api_format {
ConversionMode::None
} else {
ConversionMode::Bidirectional
};
let upstream_base_url = infer_upstream_base_url(&url);
let provider_contract = provider_api_format.clone();
let client_contract = client_api_format.clone();
let auth_header = auth_pair.map(|(name, _)| name.to_string());
let auth_value = auth_pair.map(|(_, value)| value.to_string());
let aether_contracts::RequestBody {
json_body,
body_bytes_b64,
body_ref: _body_ref,
} = body;
debug!(
event_name = "local_video_follow_up_sync_decision_payload_built",
log_type = "debug",
trace_id = %trace_id,
request_id = %trace_id,
candidate_id = ?candidate_id,
provider_id = %provider_id,
endpoint_id = %endpoint_id,
key_id = %key_id,
plan_kind,
downstream_path = %parts.uri.path(),
provider_api_format = %provider_api_format,
client_api_format = %client_api_format,
upstream_base_url = ?upstream_base_url,
upstream_url = %url,
"gateway built local video follow-up sync decision payload"
);
Ok(Some(GatewayControlSyncDecisionResponse {
action: EXECUTION_RUNTIME_SYNC_DECISION_ACTION.to_string(),
decision_kind: Some(plan_kind.to_string()),
execution_strategy: Some(execution_strategy.as_str().to_string()),
conversion_mode: Some(conversion_mode.as_str().to_string()),
request_id: Some(trace_id.to_string()),
candidate_id,
provider_name,
provider_id: Some(provider_id),
endpoint_id: Some(endpoint_id),
key_id: Some(key_id),
upstream_base_url,
upstream_url: Some(url),
provider_request_method: Some(method),
auth_header,
auth_value,
provider_api_format: Some(provider_api_format),
client_api_format: Some(client_api_format),
provider_contract: Some(provider_contract),
client_contract: Some(client_contract),
model_name,
mapped_model: None,
prompt_cache_key: None,
extra_headers: BTreeMap::new(),
provider_request_headers: headers,
provider_request_body: json_body,
provider_request_body_base64: body_bytes_b64,
content_type,
proxy,
tls_profile,
timeouts,
upstream_is_stream: false,
report_kind,
report_context,
auth_context: Some(build_execution_runtime_auth_context(&auth_context)),
}))
}
fn extract_auth_header_pair<'a>(
headers: &'a BTreeMap<String, String>,
) -> Option<(&'a str, &'a str)> {
[
"authorization",
"x-api-key",
"api-key",
"x-goog-api-key",
"proxy-authorization",
]
.into_iter()
.find_map(|name| {
headers
.iter()
.find(|(header_name, _)| header_name.eq_ignore_ascii_case(name))
.map(|(header_name, value)| (header_name.as_str(), value.as_str()))
})
}
fn infer_upstream_base_url(upstream_url: &str) -> Option<String> {
let parsed = Url::parse(upstream_url).ok()?;
let host = parsed.host_str()?;
let mut base = format!("{}://{}", parsed.scheme(), host);
if let Some(port) = parsed.port() {
base.push(':');
base.push_str(port.to_string().as_str());
}
let base_path = infer_upstream_base_path(parsed.path());
if !base_path.is_empty() {
base.push_str(base_path);
}
Some(base)
}
fn infer_upstream_base_path(path: &str) -> &str {
let trimmed = path.trim_end_matches('/');
if trimmed.is_empty() || trimmed == "/" {
return "";
}
for suffix in [
"/responses/compact",
"/responses",
"/chat/completions",
"/messages",
] {
if let Some(prefix) = trimmed.strip_suffix(suffix) {
return normalize_inferred_base_path(prefix);
}
}
for marker in ["/v1/videos", "/v1beta/"] {
if let Some((prefix, _)) = trimmed.split_once(marker) {
return normalize_inferred_base_path(prefix);
}
}
normalize_inferred_base_path(trimmed)
}
fn normalize_inferred_base_path(path: &str) -> &str {
let trimmed = path.trim_end_matches('/');
if trimmed.is_empty() || trimmed == "/" {
""
} else {
trimmed
}
}
#[cfg(test)]
mod tests {
use super::infer_upstream_base_url;
#[test]
fn infer_upstream_base_url_preserves_codex_base_path() {
assert_eq!(
infer_upstream_base_url("https://tiger.bookapi.cc/codex/responses").as_deref(),
Some("https://tiger.bookapi.cc/codex")
);
assert_eq!(
infer_upstream_base_url("https://chatgpt.com/backend-api/codex/responses").as_deref(),
Some("https://chatgpt.com/backend-api/codex")
);
}
#[test]
fn infer_upstream_base_url_preserves_nested_v1_prefix() {
assert_eq!(
infer_upstream_base_url("https://api.openai.example/custom/v1/chat/completions?mode=1")
.as_deref(),
Some("https://api.openai.example/custom/v1")
);
}
#[test]
fn infer_upstream_base_url_strips_video_operation_path() {
assert_eq!(
infer_upstream_base_url("https://video.example/nested/v1/videos/task-123/content")
.as_deref(),
Some("https://video.example/nested")
);
}
}

View File

@@ -1,83 +0,0 @@
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::{AppState, GatewayError};
#[derive(Debug, Clone)]
pub(crate) struct ResolvedLocalDecisionAuthInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub(crate) struct LocalRequestedModelDecisionInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) requested_model: String,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub(crate) struct LocalAuthenticatedDecisionInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
pub(crate) fn build_local_requested_model_decision_input(
resolved_input: ResolvedLocalDecisionAuthInput,
requested_model: String,
) -> LocalRequestedModelDecisionInput {
LocalRequestedModelDecisionInput {
auth_context: resolved_input.auth_context,
requested_model,
auth_snapshot: resolved_input.auth_snapshot,
required_capabilities: resolved_input.required_capabilities,
}
}
pub(crate) fn build_local_authenticated_decision_input(
resolved_input: ResolvedLocalDecisionAuthInput,
) -> LocalAuthenticatedDecisionInput {
LocalAuthenticatedDecisionInput {
auth_context: resolved_input.auth_context,
auth_snapshot: resolved_input.auth_snapshot,
required_capabilities: resolved_input.required_capabilities,
}
}
pub(crate) async fn resolve_local_authenticated_decision_input(
state: &AppState,
auth_context: ExecutionRuntimeAuthContext,
requested_model: Option<&str>,
explicit_required_capabilities: Option<&serde_json::Value>,
) -> Result<Option<ResolvedLocalDecisionAuthInput>, GatewayError> {
let planner_state = PlannerAppState::new(state);
let auth_snapshot = match planner_state
.read_auth_api_key_snapshot(
&auth_context.user_id,
&auth_context.api_key_id,
current_unix_secs(),
)
.await?
{
Some(snapshot) => snapshot,
None => return Ok(None),
};
let required_capabilities = planner_state
.resolve_request_candidate_required_capabilities(
&auth_context.user_id,
&auth_context.api_key_id,
requested_model,
explicit_required_capabilities,
)
.await;
Ok(Some(ResolvedLocalDecisionAuthInput {
auth_context,
auth_snapshot,
required_capabilities,
}))
}

View File

@@ -1,190 +0,0 @@
use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CandidateFailureDiagnosticKind {
RequestBodyBuild,
RequestConversion,
BodyRules,
HeaderRules,
UrlBuild,
TransportAuth,
EnvelopeBuild,
}
impl CandidateFailureDiagnosticKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::RequestBodyBuild => "request_body_build",
Self::RequestConversion => "request_conversion",
Self::BodyRules => "body_rules",
Self::HeaderRules => "header_rules",
Self::UrlBuild => "url_build",
Self::TransportAuth => "transport_auth",
Self::EnvelopeBuild => "envelope_build",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct CandidateFailureDiagnostic {
pub(crate) kind: CandidateFailureDiagnosticKind,
pub(crate) path: String,
pub(crate) message: String,
pub(crate) source: Option<String>,
pub(crate) client_api_format: Option<String>,
pub(crate) provider_api_format: Option<String>,
pub(crate) safe_to_show: bool,
}
impl CandidateFailureDiagnostic {
pub(crate) fn new(
kind: CandidateFailureDiagnosticKind,
path: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
kind,
path: path.into(),
message: message.into(),
source: None,
client_api_format: None,
provider_api_format: None,
safe_to_show: true,
}
}
pub(crate) fn source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub(crate) fn formats(
mut self,
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
) -> Self {
self.client_api_format = Some(client_api_format.into());
self.provider_api_format = Some(provider_api_format.into());
self
}
pub(crate) fn to_extra_data(&self) -> Value {
let diagnostic = self.to_value();
let mut extra_data = json!({
"failure_diagnostic": diagnostic,
});
// Compatibility for current usage UI and already persisted trace readers.
if self.kind == CandidateFailureDiagnosticKind::RequestBodyBuild {
if let Some(object) = extra_data.as_object_mut() {
object.insert(
"request_body_build_error".to_string(),
json!({
"path": self.path,
"message": self.message,
"client_api_format": self.client_api_format,
"provider_api_format": self.provider_api_format,
}),
);
}
}
extra_data
}
pub(crate) fn upstream_url_missing(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::UrlBuild,
"$.endpoint",
"无法构建上游请求地址;请检查 base_url、custom_path、API 格式和模型映射",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
pub(crate) fn header_rules_apply_failed(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::HeaderRules,
"$.endpoint.header_rules",
"Header 规则应用失败;请检查规则格式、条件配置,或是否试图覆盖受保护认证头",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
pub(crate) fn body_rules_apply_failed(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::BodyRules,
"$.endpoint.body_rules",
"Body 规则应用失败;请检查规则格式、条件配置,或规则输出是否仍是当前上游支持的请求体",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
pub(crate) fn body_rules_unsupported_for_binary_upload(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::BodyRules,
"$.endpoint.body_rules",
"二进制上传暂不支持本地应用 Body 规则;请移除该 Endpoint 的 Body 规则或改用 JSON 请求体",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
pub(crate) fn provider_request_body_missing(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::RequestBodyBuild,
"$",
"无法构建上游请求体;请检查请求体是否为支持的 JSON object以及该任务类型必需字段是否存在且取值受支持",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
pub(crate) fn envelope_build_failed(
client_api_format: impl Into<String>,
provider_api_format: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::new(
CandidateFailureDiagnosticKind::EnvelopeBuild,
"$",
"无法构建上游请求封装;请检查该 Provider 的认证配置、模型映射、Endpoint Body 规则和当前请求体是否兼容",
)
.formats(client_api_format, provider_api_format)
.source(source)
}
fn to_value(&self) -> Value {
json!({
"kind": self.kind.as_str(),
"path": self.path,
"message": self.message,
"source": self.source,
"client_api_format": self.client_api_format,
"provider_api_format": self.provider_api_format,
"safe_to_show": self.safe_to_show,
})
}
}

View File

@@ -1,85 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::planner::candidate_materialization::{
LocalAvailableCandidatePersistenceContext, LocalSkippedCandidatePersistenceContext,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum LocalCandidatePersistencePolicyKind {
StandardDecision,
SameFormatProviderDecision,
OpenAiChatDecision,
OpenAiResponsesDecision,
ImageDecision,
GeminiFilesDecision,
VideoDecision,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LocalCandidatePersistencePolicy<'a> {
pub(crate) available: LocalAvailableCandidatePersistenceContext<'a>,
pub(crate) skipped: LocalSkippedCandidatePersistenceContext<'a>,
}
pub(crate) fn build_local_candidate_persistence_policy<'a>(
auth_context: &'a ExecutionRuntimeAuthContext,
required_capabilities: Option<&'a Value>,
kind: LocalCandidatePersistencePolicyKind,
) -> LocalCandidatePersistencePolicy<'a> {
let (available_error_context, skipped_error_context, record_runtime_miss_diagnostic) =
match kind {
LocalCandidatePersistencePolicyKind::StandardDecision => (
"gateway local standard decision request candidate upsert failed",
"gateway local standard decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::SameFormatProviderDecision => (
"gateway local same-format decision request candidate upsert failed",
"gateway local same-format decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::OpenAiChatDecision => (
"gateway local openai chat decision request candidate upsert failed",
"gateway local openai chat decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::OpenAiResponsesDecision => (
"gateway local openai responses decision request candidate upsert failed",
"gateway local openai responses decision failed to persist skipped candidate",
true,
),
LocalCandidatePersistencePolicyKind::ImageDecision => (
"gateway local openai image decision request candidate upsert failed",
"gateway local openai image decision failed to persist skipped candidate",
false,
),
LocalCandidatePersistencePolicyKind::GeminiFilesDecision => (
"gateway local gemini files request candidate upsert failed",
"gateway local gemini files failed to persist skipped candidate",
false,
),
LocalCandidatePersistencePolicyKind::VideoDecision => (
"gateway local video decision request candidate upsert failed",
"gateway local video decision failed to persist skipped candidate",
false,
),
};
LocalCandidatePersistencePolicy {
available: LocalAvailableCandidatePersistenceContext {
user_id: &auth_context.user_id,
api_key_id: &auth_context.api_key_id,
required_capabilities,
error_context: available_error_context,
},
skipped: LocalSkippedCandidatePersistenceContext {
user_id: &auth_context.user_id,
api_key_id: &auth_context.api_key_id,
required_capabilities,
error_context: skipped_error_context,
record_runtime_miss_diagnostic,
},
}
}

View File

@@ -1,177 +0,0 @@
use aether_contracts::{ExecutionPlan, RequestBody};
use super::{
augment_sync_report_context, take_non_empty_string, LocalStreamPlanAndReport,
LocalSyncPlanAndReport,
};
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) fn build_passthrough_sync_plan_from_decision(
parts: &http::request::Parts,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalSyncPlanAndReport>, GatewayError> {
let mut payload = payload;
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(upstream_url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
let ignored_provider_request_body = serde_json::Value::Null;
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&ignored_provider_request_body,
)?;
let request_body = resolve_passthrough_sync_request_body(
payload.provider_request_body.take(),
payload.provider_request_body_base64.take(),
);
let provider_request_method = take_non_empty_string(&mut payload.provider_request_method);
let content_type = payload
.content_type
.take()
.or_else(|| provider_request_headers.get("content-type").cloned());
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: provider_request_method.unwrap_or_else(|| parts.method.to_string()),
url: upstream_url,
headers: provider_request_headers,
content_type,
content_encoding: None,
body: request_body,
stream: false,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalSyncPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
pub(crate) fn build_passthrough_stream_plan_from_decision(
parts: &http::request::Parts,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalStreamPlanAndReport>, GatewayError> {
let mut payload = payload;
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(upstream_url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
let content_type = payload
.content_type
.take()
.or_else(|| provider_request_headers.get("content-type").cloned());
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: parts.method.to_string(),
url: upstream_url,
headers: provider_request_headers,
content_type,
content_encoding: None,
body: RequestBody {
json_body: None,
body_bytes_b64: None,
body_ref: None,
},
stream: true,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalStreamPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context: payload.report_context,
}))
}
fn resolve_passthrough_sync_request_body(
provider_request_body: Option<serde_json::Value>,
provider_request_body_base64: Option<String>,
) -> RequestBody {
if let Some(body_bytes_b64) = provider_request_body_base64.and_then(trim_owned_non_empty_string)
{
return RequestBody {
json_body: None,
body_bytes_b64: Some(body_bytes_b64),
body_ref: None,
};
}
match provider_request_body.unwrap_or(serde_json::Value::Null) {
serde_json::Value::Null => RequestBody {
json_body: None,
body_bytes_b64: None,
body_ref: None,
},
other => RequestBody::from_json(other),
}
}
fn trim_owned_non_empty_string(value: String) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.len() == value.len() {
return Some(value);
}
Some(trimmed.to_owned())
}

View File

@@ -1,134 +0,0 @@
use crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata;
use crate::ai_pipeline::transport::auth::{resolve_local_gemini_auth, resolve_local_standard_auth};
use crate::ai_pipeline::transport::claude_code::local_claude_code_transport_unsupported_reason_with_network;
use crate::ai_pipeline::transport::kiro::local_kiro_request_transport_unsupported_reason_with_network;
use crate::ai_pipeline::transport::policy::{
local_gemini_transport_unsupported_reason_with_network,
local_standard_transport_unsupported_reason_with_network,
};
use crate::ai_pipeline::transport::vertex::{
is_vertex_api_key_transport_context,
local_vertex_api_key_gemini_transport_unsupported_reason_with_network,
};
use crate::ai_pipeline::GatewayProviderTransportSnapshot;
use super::super::LocalSameFormatProviderFamily;
pub(super) struct SameFormatProviderRequestBehavior {
pub(super) is_antigravity: bool,
pub(super) is_claude_code: bool,
pub(super) is_vertex: bool,
pub(super) is_kiro: bool,
pub(super) upstream_is_stream: bool,
pub(super) report_kind: &'static str,
}
pub(super) fn classify_same_format_provider_request_behavior(
transport: &GatewayProviderTransportSnapshot,
spec_metadata: LocalExecutionSurfaceSpecMetadata,
) -> SameFormatProviderRequestBehavior {
let is_antigravity = transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("antigravity");
let is_claude_code = transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("claude_code");
let is_vertex = is_vertex_api_key_transport_context(transport);
let is_kiro = transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("kiro");
let default_report_kind = spec_metadata
.report_kind
.expect("same-format provider specs should declare report kind");
let upstream_is_stream = is_kiro || is_antigravity || spec_metadata.require_streaming;
let report_kind = if is_kiro && !spec_metadata.require_streaming {
"claude_cli_sync_finalize"
} else if is_antigravity && !spec_metadata.require_streaming {
match default_report_kind {
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
"gemini_cli_sync_success" => "gemini_cli_sync_finalize",
_ => default_report_kind,
}
} else {
default_report_kind
};
SameFormatProviderRequestBehavior {
is_antigravity,
is_claude_code,
is_vertex,
is_kiro,
upstream_is_stream,
report_kind,
}
}
pub(super) fn same_format_provider_transport_supported(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> bool {
same_format_provider_transport_unsupported_reason(behavior, transport, family, api_format)
.is_none()
}
pub(super) fn same_format_provider_transport_unsupported_reason(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> Option<&'static str> {
if behavior.is_kiro {
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if behavior.is_antigravity {
None
} else if behavior.is_claude_code {
local_claude_code_transport_unsupported_reason_with_network(transport, api_format)
} else if behavior.is_vertex {
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(transport)
} else {
match family {
LocalSameFormatProviderFamily::Standard => {
local_standard_transport_unsupported_reason_with_network(transport, api_format)
}
LocalSameFormatProviderFamily::Gemini => {
local_gemini_transport_unsupported_reason_with_network(transport, api_format)
}
}
}
}
pub(super) fn should_try_same_format_provider_oauth_auth(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
) -> bool {
behavior.is_kiro
|| matches!(family, LocalSameFormatProviderFamily::Standard)
&& resolve_local_standard_auth(transport).is_none()
|| matches!(family, LocalSameFormatProviderFamily::Gemini)
&& !behavior.is_vertex
&& resolve_local_gemini_auth(transport).is_none()
}
pub(super) fn resolve_same_format_provider_direct_auth(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
) -> Option<(String, String)> {
if behavior.is_vertex {
None
} else {
match family {
LocalSameFormatProviderFamily::Standard => resolve_local_standard_auth(transport),
LocalSameFormatProviderFamily::Gemini => resolve_local_gemini_auth(transport),
}
}
}

View File

@@ -1,52 +0,0 @@
use serde_json::Value;
use super::super::{
apply_local_body_rules, build_kiro_provider_request_body, sanitize_claude_code_request_body,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
};
pub(crate) fn build_same_format_provider_request_body(
body_json: &Value,
mapped_model: &str,
spec: LocalSameFormatProviderSpec,
body_rules: Option<&Value>,
upstream_is_stream: bool,
kiro_auth: Option<&crate::ai_pipeline::transport::kiro::KiroRequestAuth>,
is_claude_code: bool,
) -> Option<Value> {
if let Some(kiro_auth) = kiro_auth {
return build_kiro_provider_request_body(
body_json,
mapped_model,
&kiro_auth.auth_config,
body_rules,
);
}
let request_body_object = body_json.as_object()?;
let mut provider_request_body = serde_json::Map::from_iter(
request_body_object
.iter()
.map(|(key, value)| (key.clone(), value.clone())),
);
match spec.family {
LocalSameFormatProviderFamily::Standard => {
provider_request_body
.insert("model".to_string(), Value::String(mapped_model.to_string()));
if upstream_is_stream {
provider_request_body.insert("stream".to_string(), Value::Bool(true));
}
}
LocalSameFormatProviderFamily::Gemini => {
provider_request_body.remove("model");
}
}
let mut provider_request_body = Value::Object(provider_request_body);
if is_claude_code {
sanitize_claude_code_request_body(&mut provider_request_body);
}
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
Some(provider_request_body)
}

View File

@@ -1,25 +0,0 @@
use crate::ai_pipeline::GatewayProviderTransportSnapshot;
use super::super::LocalSameFormatProviderSpec;
pub(crate) fn build_same_format_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
spec: LocalSameFormatProviderSpec,
upstream_is_stream: bool,
kiro_auth: Option<&crate::ai_pipeline::transport::kiro::KiroRequestAuth>,
) -> Option<String> {
maybe_add_gemini_stream_alt_sse(crate::ai_pipeline::build_provider_transport_request_url(
transport,
spec.api_format,
Some(mapped_model),
upstream_is_stream,
parts.uri.query(),
kiro_auth.map(|auth| auth.auth_config.effective_api_region()),
))
}
fn maybe_add_gemini_stream_alt_sse(url: Option<String>) -> Option<String> {
url
}

View File

@@ -1,90 +0,0 @@
use std::collections::BTreeMap;
use aether_contracts::{ExecutionTimeouts, ProxySnapshot};
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, GatewayControlSyncDecisionResponse};
use crate::{EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION};
pub(crate) struct LocalExecutionDecisionResponseParts {
pub(crate) decision_is_stream: bool,
pub(crate) decision_kind: String,
pub(crate) execution_strategy: ExecutionStrategy,
pub(crate) conversion_mode: ConversionMode,
pub(crate) request_id: String,
pub(crate) candidate_id: String,
pub(crate) provider_name: String,
pub(crate) provider_id: String,
pub(crate) endpoint_id: String,
pub(crate) key_id: String,
pub(crate) upstream_base_url: String,
pub(crate) upstream_url: String,
pub(crate) provider_request_method: Option<String>,
pub(crate) auth_header: Option<String>,
pub(crate) auth_value: Option<String>,
pub(crate) provider_api_format: String,
pub(crate) client_api_format: String,
pub(crate) model_name: String,
pub(crate) mapped_model: String,
pub(crate) prompt_cache_key: Option<String>,
pub(crate) provider_request_headers: BTreeMap<String, String>,
pub(crate) provider_request_body: Option<serde_json::Value>,
pub(crate) provider_request_body_base64: Option<String>,
pub(crate) content_type: Option<String>,
pub(crate) proxy: Option<ProxySnapshot>,
pub(crate) tls_profile: Option<String>,
pub(crate) timeouts: Option<ExecutionTimeouts>,
pub(crate) upstream_is_stream: bool,
pub(crate) report_kind: Option<String>,
pub(crate) report_context: Option<serde_json::Value>,
pub(crate) auth_context: ExecutionRuntimeAuthContext,
}
pub(crate) fn build_local_execution_decision_response(
parts: LocalExecutionDecisionResponseParts,
) -> GatewayControlSyncDecisionResponse {
GatewayControlSyncDecisionResponse {
action: local_execution_decision_action(parts.decision_is_stream).to_string(),
decision_kind: Some(parts.decision_kind),
execution_strategy: Some(parts.execution_strategy.as_str().to_string()),
conversion_mode: Some(parts.conversion_mode.as_str().to_string()),
request_id: Some(parts.request_id),
candidate_id: Some(parts.candidate_id),
provider_name: Some(parts.provider_name),
provider_id: Some(parts.provider_id),
endpoint_id: Some(parts.endpoint_id),
key_id: Some(parts.key_id),
upstream_base_url: Some(parts.upstream_base_url),
upstream_url: Some(parts.upstream_url),
provider_request_method: parts.provider_request_method,
auth_header: parts.auth_header,
auth_value: parts.auth_value,
provider_api_format: Some(parts.provider_api_format.clone()),
client_api_format: Some(parts.client_api_format.clone()),
provider_contract: Some(parts.provider_api_format),
client_contract: Some(parts.client_api_format),
model_name: Some(parts.model_name),
mapped_model: Some(parts.mapped_model),
prompt_cache_key: parts.prompt_cache_key,
extra_headers: BTreeMap::new(),
provider_request_headers: parts.provider_request_headers,
provider_request_body: parts.provider_request_body,
provider_request_body_base64: parts.provider_request_body_base64,
content_type: parts.content_type,
proxy: parts.proxy,
tls_profile: parts.tls_profile,
timeouts: parts.timeouts,
upstream_is_stream: parts.upstream_is_stream,
report_kind: parts.report_kind,
report_context: parts.report_context,
auth_context: Some(parts.auth_context),
}
}
pub(crate) fn local_execution_decision_action(decision_is_stream: bool) -> &'static str {
if decision_is_stream {
EXECUTION_RUNTIME_STREAM_DECISION_ACTION
} else {
EXECUTION_RUNTIME_SYNC_DECISION_ACTION
}
}

View File

@@ -1,352 +0,0 @@
use std::collections::BTreeMap;
use aether_scheduler_core::SchedulerRankingOutcome;
use serde_json::{Map, Value};
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::planner::candidate_metadata::append_ranking_metadata_to_object;
use crate::ai_pipeline::{request_origin_from_headers, RequestOrigin};
use crate::orchestration::ExecutionAttemptIdentity;
pub(crate) struct LocalExecutionReportContextParts<'a> {
pub(crate) auth_context: &'a ExecutionRuntimeAuthContext,
pub(crate) request_id: &'a str,
pub(crate) candidate_id: &'a str,
pub(crate) attempt_identity: ExecutionAttemptIdentity,
pub(crate) model: &'a str,
pub(crate) provider_name: &'a str,
pub(crate) provider_id: &'a str,
pub(crate) endpoint_id: &'a str,
pub(crate) key_id: &'a str,
pub(crate) key_name: Option<&'a str>,
pub(crate) model_id: Option<&'a str>,
pub(crate) global_model_id: Option<&'a str>,
pub(crate) global_model_name: Option<&'a str>,
pub(crate) provider_api_format: &'a str,
pub(crate) client_api_format: &'a str,
pub(crate) mapped_model: Option<&'a str>,
pub(crate) candidate_group_id: Option<&'a str>,
pub(crate) ranking: Option<&'a SchedulerRankingOutcome>,
pub(crate) upstream_url: Option<&'a str>,
pub(crate) header_rules: Option<&'a Value>,
pub(crate) body_rules: Option<&'a Value>,
pub(crate) provider_request_method: Option<Value>,
pub(crate) provider_request_headers: Option<&'a BTreeMap<String, String>>,
pub(crate) original_headers: &'a http::HeaderMap,
pub(crate) request_origin: Option<RequestOrigin>,
pub(crate) original_request_body_json: Option<&'a Value>,
pub(crate) original_request_body_base64: Option<&'a str>,
pub(crate) client_requested_stream: bool,
pub(crate) upstream_is_stream: bool,
pub(crate) has_envelope: bool,
pub(crate) needs_conversion: bool,
pub(crate) extra_fields: Map<String, Value>,
}
pub(crate) fn build_local_execution_report_context(
parts: LocalExecutionReportContextParts<'_>,
) -> Value {
let mut object = Map::new();
object.insert(
"user_id".to_string(),
Value::String(parts.auth_context.user_id.clone()),
);
object.insert(
"api_key_id".to_string(),
Value::String(parts.auth_context.api_key_id.clone()),
);
object.insert(
"api_key_is_standalone".to_string(),
Value::Bool(parts.auth_context.api_key_is_standalone),
);
object.insert(
"username".to_string(),
parts
.auth_context
.username
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
);
object.insert(
"api_key_name".to_string(),
parts
.auth_context
.api_key_name
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
);
object.insert(
"request_id".to_string(),
Value::String(parts.request_id.to_string()),
);
object.insert(
"candidate_id".to_string(),
Value::String(parts.candidate_id.to_string()),
);
object.insert(
"candidate_index".to_string(),
Value::Number(parts.attempt_identity.candidate_index.into()),
);
object.insert(
"retry_index".to_string(),
Value::Number(parts.attempt_identity.retry_index.into()),
);
object.insert("model".to_string(), Value::String(parts.model.to_string()));
object.insert(
"provider_name".to_string(),
Value::String(parts.provider_name.to_string()),
);
object.insert(
"provider_id".to_string(),
Value::String(parts.provider_id.to_string()),
);
object.insert(
"endpoint_id".to_string(),
Value::String(parts.endpoint_id.to_string()),
);
object.insert(
"key_id".to_string(),
Value::String(parts.key_id.to_string()),
);
object.insert(
"provider_api_format".to_string(),
Value::String(parts.provider_api_format.to_string()),
);
object.insert(
"client_api_format".to_string(),
Value::String(parts.client_api_format.to_string()),
);
object.insert(
"original_headers".to_string(),
serde_json::to_value(crate::ai_pipeline::collect_control_headers(
parts.original_headers,
))
.expect("control headers should serialize"),
);
object.insert(
"original_request_body".to_string(),
crate::ai_pipeline::build_report_context_original_request_echo(
parts.original_request_body_json,
parts.original_request_body_base64,
)
.unwrap_or(Value::Null),
);
let RequestOrigin {
client_ip,
user_agent,
} = parts
.request_origin
.unwrap_or_else(|| request_origin_from_headers(parts.original_headers));
if let Some(client_ip) = client_ip {
object.insert("client_ip".to_string(), Value::String(client_ip));
}
if let Some(user_agent) = user_agent {
object.insert("user_agent".to_string(), Value::String(user_agent));
}
object.insert(
"client_requested_stream".to_string(),
Value::Bool(parts.client_requested_stream),
);
object.insert(
"upstream_is_stream".to_string(),
Value::Bool(parts.upstream_is_stream),
);
object.insert("has_envelope".to_string(), Value::Bool(parts.has_envelope));
object.insert(
"needs_conversion".to_string(),
Value::Bool(parts.needs_conversion),
);
if let Some(key_name) = parts.key_name {
object.insert("key_name".to_string(), Value::String(key_name.to_string()));
}
if let Some(model_id) = parts.model_id {
object.insert("model_id".to_string(), Value::String(model_id.to_string()));
}
if let Some(global_model_id) = parts.global_model_id {
object.insert(
"global_model_id".to_string(),
Value::String(global_model_id.to_string()),
);
}
if let Some(global_model_name) = parts.global_model_name {
object.insert(
"global_model_name".to_string(),
Value::String(global_model_name.to_string()),
);
}
if let Some(mapped_model) = parts.mapped_model {
object.insert(
"mapped_model".to_string(),
Value::String(mapped_model.to_string()),
);
}
if let Some(candidate_group_id) = parts.candidate_group_id {
object.insert(
"candidate_group_id".to_string(),
Value::String(candidate_group_id.to_string()),
);
}
if let Some(ranking) = parts.ranking {
append_ranking_metadata_to_object(&mut object, ranking);
}
if let Some(upstream_url) = parts.upstream_url {
object.insert(
"upstream_url".to_string(),
Value::String(upstream_url.to_string()),
);
}
if let Some(header_rules) = parts.header_rules {
object.insert("header_rules".to_string(), header_rules.clone());
}
if let Some(body_rules) = parts.body_rules {
object.insert("body_rules".to_string(), body_rules.clone());
}
if let Some(provider_request_method) = parts.provider_request_method {
object.insert(
"provider_request_method".to_string(),
provider_request_method,
);
}
if let Some(provider_request_headers) = parts.provider_request_headers {
object.insert(
"provider_request_headers".to_string(),
serde_json::to_value(provider_request_headers)
.expect("provider request headers should serialize"),
);
}
if let Some(pool_key_index) = parts.attempt_identity.pool_key_index {
object.insert(
"pool_key_index".to_string(),
Value::Number(pool_key_index.into()),
);
}
object.extend(parts.extra_fields);
Value::Object(object)
}
pub(crate) fn provider_stream_event_api_format_for_provider_type(
provider_type: &str,
) -> Option<&'static str> {
match provider_type.trim().to_ascii_lowercase().as_str() {
"codex" => Some("openai:responses"),
_ => None,
}
}
pub(crate) fn insert_provider_stream_event_api_format(
extra_fields: &mut Map<String, Value>,
provider_type: &str,
) {
if let Some(api_format) = provider_stream_event_api_format_for_provider_type(provider_type) {
extra_fields.insert(
"provider_stream_event_api_format".to_string(),
Value::String(api_format.to_string()),
);
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use super::{
build_local_execution_report_context, provider_stream_event_api_format_for_provider_type,
LocalExecutionReportContextParts,
};
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::RequestOrigin;
use crate::orchestration::ExecutionAttemptIdentity;
#[test]
fn codex_provider_uses_openai_responses_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("codex"),
Some("openai:responses")
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("CODEX"),
Some("openai:responses")
);
}
#[test]
fn ordinary_providers_do_not_override_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("openai"),
None
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("anthropic"),
None
);
}
#[test]
fn local_execution_report_context_records_request_origin() {
let auth_context = ExecutionRuntimeAuthContext {
user_id: "user-1".to_string(),
api_key_id: "api-key-1".to_string(),
username: None,
api_key_name: None,
balance_remaining: None,
access_allowed: true,
api_key_is_standalone: false,
};
let original_headers = http::HeaderMap::new();
let provider_request_headers = BTreeMap::new();
let report_context =
build_local_execution_report_context(LocalExecutionReportContextParts {
auth_context: &auth_context,
request_id: "trace-1",
candidate_id: "candidate-1",
attempt_identity: ExecutionAttemptIdentity::new(0, 0),
model: "gpt-5",
provider_name: "OpenAI",
provider_id: "provider-1",
endpoint_id: "endpoint-1",
key_id: "key-1",
key_name: None,
model_id: None,
global_model_id: None,
global_model_name: None,
provider_api_format: "openai:chat",
client_api_format: "openai:chat",
mapped_model: None,
candidate_group_id: None,
ranking: None,
upstream_url: None,
header_rules: None,
body_rules: None,
provider_request_method: None,
provider_request_headers: Some(&provider_request_headers),
original_headers: &original_headers,
request_origin: Some(RequestOrigin {
client_ip: Some("203.0.113.8".to_string()),
user_agent: Some("Claude-Code/1.0".to_string()),
}),
original_request_body_json: Some(&json!({"model": "gpt-5"})),
original_request_body_base64: None,
client_requested_stream: false,
upstream_is_stream: false,
has_envelope: false,
needs_conversion: false,
extra_fields: Map::new(),
});
assert_eq!(
report_context["client_ip"],
Value::String("203.0.113.8".to_string())
);
assert_eq!(
report_context["user_agent"],
Value::String("Claude-Code/1.0".to_string())
);
}
}

View File

@@ -1,137 +0,0 @@
use crate::ai_pipeline::planner::common::{
apply_local_candidate_evaluation_progress, apply_local_candidate_terminal_plan_reason,
build_local_runtime_miss_diagnostic,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::{AppState, LocalExecutionRuntimeMissDiagnostic};
pub(crate) fn set_local_runtime_miss_diagnostic_reason(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
reason: &str,
) {
state.set_local_execution_runtime_miss_diagnostic(
trace_id,
build_local_runtime_miss_diagnostic(decision, plan_kind, requested_model, reason),
);
}
pub(crate) fn build_local_runtime_execution_exhausted_diagnostic(
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) -> LocalExecutionRuntimeMissDiagnostic {
let mut diagnostic = build_local_runtime_miss_diagnostic(
decision,
plan_kind,
requested_model,
"execution_runtime_candidates_exhausted",
);
diagnostic.candidate_count = Some(candidate_count);
diagnostic
}
pub(crate) fn set_local_runtime_execution_exhausted_diagnostic(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) {
state.set_local_execution_runtime_miss_diagnostic(
trace_id,
build_local_runtime_execution_exhausted_diagnostic(
decision,
plan_kind,
requested_model,
candidate_count,
),
);
}
pub(crate) fn build_local_runtime_candidate_evaluation_diagnostic(
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) -> LocalExecutionRuntimeMissDiagnostic {
let mut diagnostic = build_local_runtime_miss_diagnostic(
decision,
plan_kind,
requested_model,
"candidate_evaluation_incomplete",
);
apply_local_candidate_evaluation_progress(&mut diagnostic, candidate_count);
diagnostic
}
pub(crate) fn set_local_runtime_candidate_evaluation_diagnostic(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) {
state.set_local_execution_runtime_miss_diagnostic(
trace_id,
build_local_runtime_candidate_evaluation_diagnostic(
decision,
plan_kind,
requested_model,
candidate_count,
),
);
}
pub(crate) fn apply_local_runtime_candidate_evaluation_progress(
state: &AppState,
trace_id: &str,
candidate_count: usize,
) {
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
apply_local_candidate_evaluation_progress(diagnostic, candidate_count);
});
}
pub(crate) fn apply_local_runtime_candidate_evaluation_progress_preserving_candidate_signal(
state: &AppState,
trace_id: &str,
candidate_count: usize,
) {
let preserve_existing_candidate_signal = candidate_count == 0
&& state.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id);
if preserve_existing_candidate_signal {
return;
}
apply_local_runtime_candidate_evaluation_progress(state, trace_id, candidate_count);
}
pub(crate) fn apply_local_runtime_candidate_terminal_reason(
state: &AppState,
trace_id: &str,
no_plan_reason: &'static str,
) {
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
apply_local_candidate_terminal_plan_reason(diagnostic, no_plan_reason);
});
}
pub(crate) fn record_local_runtime_candidate_skip_reason(
state: &AppState,
trace_id: &str,
skip_reason: &'static str,
) {
state.mutate_local_execution_runtime_miss_diagnostic(trace_id, |diagnostic| {
*diagnostic
.skip_reasons
.entry(skip_reason.to_string())
.or_insert(0) += 1;
*diagnostic.skipped_candidate_count.get_or_insert(0) += 1;
});
}

View File

@@ -1,152 +0,0 @@
use crate::ai_pipeline::planner::common::RequestedModelFamily;
use crate::ai_pipeline::planner::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::{
GatewayControlSyncDecisionResponse, LocalGeminiFilesSpec, LocalOpenAiImageSpec,
LocalOpenAiResponsesSpec, LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
LocalStandardSourceFamily, LocalStandardSpec, LocalVideoCreateFamily, LocalVideoCreateSpec,
};
use crate::GatewayError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LocalExecutionSurfaceSpecMetadata {
pub(crate) api_format: &'static str,
pub(crate) decision_kind: &'static str,
pub(crate) report_kind: Option<&'static str>,
pub(crate) require_streaming: bool,
pub(crate) requested_model_family: Option<RequestedModelFamily>,
}
pub(crate) fn requested_model_family_for_standard_source(
family: LocalStandardSourceFamily,
) -> RequestedModelFamily {
match family {
LocalStandardSourceFamily::Standard => RequestedModelFamily::Standard,
LocalStandardSourceFamily::Gemini => RequestedModelFamily::Gemini,
}
}
pub(crate) fn local_standard_spec_metadata(
spec: LocalStandardSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,
decision_kind: spec.decision_kind,
report_kind: Some(spec.report_kind),
require_streaming: spec.require_streaming,
requested_model_family: Some(requested_model_family_for_standard_source(spec.family)),
}
}
pub(crate) fn local_same_format_provider_spec_metadata(
spec: LocalSameFormatProviderSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,
decision_kind: spec.decision_kind,
report_kind: Some(spec.report_kind),
require_streaming: spec.require_streaming,
requested_model_family: Some(requested_model_family_for_same_format_provider(spec.family)),
}
}
pub(crate) fn local_openai_responses_spec_metadata(
spec: LocalOpenAiResponsesSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,
decision_kind: spec.decision_kind,
report_kind: Some(spec.report_kind),
require_streaming: spec.require_streaming,
requested_model_family: None,
}
}
pub(crate) fn local_gemini_files_spec_metadata(
spec: LocalGeminiFilesSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: "gemini:files",
decision_kind: spec.decision_kind,
report_kind: spec.report_kind,
require_streaming: spec.require_streaming,
requested_model_family: None,
}
}
pub(crate) fn local_openai_image_spec_metadata(
spec: LocalOpenAiImageSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,
decision_kind: spec.decision_kind,
report_kind: Some(spec.report_kind),
require_streaming: spec.require_streaming,
requested_model_family: Some(RequestedModelFamily::Standard),
}
}
pub(crate) fn local_video_create_spec_metadata(
spec: LocalVideoCreateSpec,
) -> LocalExecutionSurfaceSpecMetadata {
LocalExecutionSurfaceSpecMetadata {
api_format: spec.api_format,
decision_kind: spec.decision_kind,
report_kind: Some(spec.report_kind),
require_streaming: false,
requested_model_family: Some(requested_model_family_for_video_create(spec.family)),
}
}
pub(crate) fn requested_model_family_for_same_format_provider(
family: LocalSameFormatProviderFamily,
) -> RequestedModelFamily {
match family {
LocalSameFormatProviderFamily::Standard => RequestedModelFamily::Standard,
LocalSameFormatProviderFamily::Gemini => RequestedModelFamily::Gemini,
}
}
pub(crate) fn requested_model_family_for_video_create(
family: LocalVideoCreateFamily,
) -> RequestedModelFamily {
match family {
LocalVideoCreateFamily::OpenAi => RequestedModelFamily::Standard,
LocalVideoCreateFamily::Gemini => RequestedModelFamily::Gemini,
}
}
pub(crate) fn build_sync_plan_from_requested_model_family(
family: RequestedModelFamily,
parts: &http::request::Parts,
body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalSyncPlanAndReport>, GatewayError> {
match family {
RequestedModelFamily::Standard => {
build_standard_sync_plan_from_decision(parts, body_json, payload)
}
RequestedModelFamily::Gemini => {
build_gemini_sync_plan_from_decision(parts, body_json, payload)
}
}
}
pub(crate) fn build_stream_plan_from_requested_model_family(
family: RequestedModelFamily,
parts: &http::request::Parts,
body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalStreamPlanAndReport>, GatewayError> {
match family {
RequestedModelFamily::Standard => {
build_standard_stream_plan_from_decision(parts, body_json, payload, false)
}
RequestedModelFamily::Gemini => {
build_gemini_stream_plan_from_decision(parts, body_json, payload)
}
}
}

View File

@@ -1,285 +0,0 @@
use std::collections::BTreeSet;
use tracing::warn;
use crate::ai_pipeline::conversion::{request_candidate_api_formats, request_conversion_kind};
use crate::ai_pipeline::planner::candidate_materialization::{
persist_available_local_execution_candidates_with_context,
persist_skipped_local_execution_candidates_with_context,
remember_first_local_candidate_affinity,
};
use crate::ai_pipeline::planner::candidate_metadata::{
build_local_execution_candidate_contract_metadata,
build_local_execution_candidate_contract_metadata_for_candidate,
LocalExecutionCandidateMetadataParts,
};
use crate::ai_pipeline::planner::candidate_resolution::{
extract_pool_sticky_session_token, resolve_and_rank_local_execution_candidates,
SkippedLocalExecutionCandidate,
};
use crate::ai_pipeline::planner::candidate_source::auth_snapshot_allows_cross_format_candidate;
use crate::ai_pipeline::planner::common::extract_requested_model_from_request;
use crate::ai_pipeline::planner::decision_input::{
build_local_requested_model_decision_input, resolve_local_authenticated_decision_input,
};
use crate::ai_pipeline::planner::materialization_policy::{
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
};
use crate::ai_pipeline::planner::spec_metadata::local_standard_spec_metadata;
use crate::ai_pipeline::PlannerAppState;
use crate::ai_pipeline::{
resolve_local_decision_execution_runtime_auth_context, ConversionMode, ExecutionStrategy,
GatewayControlDecision,
};
use crate::clock::current_unix_secs;
use crate::{AppState, GatewayError};
use super::{LocalStandardCandidateAttempt, LocalStandardDecisionInput, LocalStandardSpec};
pub(super) async fn resolve_local_standard_decision_input(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
spec: LocalStandardSpec,
) -> Option<LocalStandardDecisionInput> {
let spec_metadata = local_standard_spec_metadata(spec);
let Some(auth_context) = resolve_local_decision_execution_runtime_auth_context(decision) else {
return None;
};
let requested_model = extract_requested_model_from_request(
parts,
body_json,
spec_metadata
.requested_model_family
.expect("standard specs should declare requested-model family"),
)?;
let resolved_input = match resolve_local_authenticated_decision_input(
state,
auth_context,
Some(requested_model.as_str()),
None,
)
.await
{
Ok(Some(resolved_input)) => resolved_input,
Ok(None) => return None,
Err(err) => {
warn!(
trace_id = %trace_id,
api_format = spec_metadata.api_format,
error = ?err,
"gateway local standard decision auth snapshot read failed"
);
return None;
}
};
Some(build_local_requested_model_decision_input(
resolved_input,
requested_model,
))
}
pub(super) async fn materialize_local_standard_candidate_attempts(
state: &AppState,
trace_id: &str,
input: &LocalStandardDecisionInput,
body_json: &serde_json::Value,
spec: LocalStandardSpec,
) -> Result<(Vec<LocalStandardCandidateAttempt>, usize), GatewayError> {
let spec_metadata = local_standard_spec_metadata(spec);
let planner_state = PlannerAppState::new(state);
let sticky_session_token = extract_pool_sticky_session_token(body_json);
let persistence_policy = build_local_candidate_persistence_policy(
&input.auth_context,
input.required_capabilities.as_ref(),
LocalCandidatePersistencePolicyKind::StandardDecision,
);
let mut seen_candidates = BTreeSet::new();
let mut seen_skipped_candidates = BTreeSet::new();
let mut candidates = Vec::new();
let mut preselection_skipped = Vec::new();
for candidate_api_format in
request_candidate_api_formats(spec_metadata.api_format, spec_metadata.require_streaming)
{
let auth_snapshot = if candidate_api_format == spec_metadata.api_format {
Some(&input.auth_snapshot)
} else {
None
};
let (mut selected_candidates, skipped_candidates) = planner_state
.list_selectable_candidates_with_skip_reasons(
candidate_api_format,
&input.requested_model,
spec_metadata.require_streaming,
input.required_capabilities.as_ref(),
auth_snapshot,
current_unix_secs(),
)
.await?;
if auth_snapshot.is_none() {
selected_candidates.retain(|candidate| {
auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
candidate,
)
});
}
for skipped_candidate in skipped_candidates {
if auth_snapshot.is_none()
&& !auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
&skipped_candidate.candidate,
)
{
continue;
}
let candidate_key = format!(
"{}:{}:{}:{}:{}:{}",
skipped_candidate.candidate.provider_id,
skipped_candidate.candidate.endpoint_id,
skipped_candidate.candidate.key_id,
skipped_candidate.candidate.model_id,
skipped_candidate.candidate.selected_provider_model_name,
skipped_candidate.candidate.endpoint_api_format,
);
if seen_skipped_candidates.insert(candidate_key) {
preselection_skipped.push(SkippedLocalExecutionCandidate {
candidate: skipped_candidate.candidate,
skip_reason: skipped_candidate.skip_reason,
transport: None,
ranking: None,
extra_data: None,
});
}
}
for candidate in selected_candidates {
let candidate_key = format!(
"{}:{}:{}:{}:{}:{}",
candidate.provider_id,
candidate.endpoint_id,
candidate.key_id,
candidate.model_id,
candidate.selected_provider_model_name,
candidate.endpoint_api_format,
);
if seen_candidates.insert(candidate_key) {
candidates.push(candidate);
}
}
}
let (candidates, skipped_candidates) = resolve_and_rank_local_execution_candidates(
planner_state,
candidates,
spec_metadata.api_format,
&input.requested_model,
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
)
.await;
let skipped_candidates = preselection_skipped
.into_iter()
.chain(skipped_candidates)
.map(|mut skipped_candidate| {
let provider_api_format = skipped_candidate
.transport
.as_ref()
.map(|transport| transport.endpoint.api_format.trim().to_ascii_lowercase())
.unwrap_or_else(|| {
skipped_candidate
.candidate
.endpoint_api_format
.trim()
.to_ascii_lowercase()
});
let execution_strategy = if provider_api_format == spec_metadata.api_format {
ExecutionStrategy::LocalSameFormat
} else {
ExecutionStrategy::LocalCrossFormat
};
let conversion_mode =
if request_conversion_kind(spec_metadata.api_format, provider_api_format.as_str())
.is_some()
{
ConversionMode::Bidirectional
} else {
ConversionMode::None
};
skipped_candidate.extra_data = Some(
build_local_execution_candidate_contract_metadata_for_candidate(
&skipped_candidate.candidate,
skipped_candidate.transport_ref(),
provider_api_format.as_str(),
spec_metadata.api_format,
serde_json::Map::new(),
execution_strategy,
conversion_mode,
provider_api_format.as_str(),
),
);
skipped_candidate
})
.collect::<Vec<_>>();
let candidate_count = candidates.len() + skipped_candidates.len();
remember_first_local_candidate_affinity(
planner_state,
Some(&input.auth_snapshot),
spec_metadata.api_format,
Some(&input.requested_model),
&candidates,
);
let available_candidate_count = candidates.len() as u32;
let attempts = persist_available_local_execution_candidates_with_context(
planner_state,
trace_id,
persistence_policy.available,
candidates,
|eligible| {
let provider_api_format = eligible.provider_api_format.clone();
let execution_strategy = if provider_api_format == spec_metadata.api_format {
ExecutionStrategy::LocalSameFormat
} else {
ExecutionStrategy::LocalCrossFormat
};
let conversion_mode =
if request_conversion_kind(spec_metadata.api_format, provider_api_format.as_str())
.is_some()
{
ConversionMode::Bidirectional
} else {
ConversionMode::None
};
Some(build_local_execution_candidate_contract_metadata(
LocalExecutionCandidateMetadataParts {
eligible,
provider_api_format: provider_api_format.as_str(),
client_api_format: spec_metadata.api_format,
extra_fields: serde_json::Map::new(),
},
execution_strategy,
conversion_mode,
eligible.candidate.endpoint_api_format.as_str(),
))
},
)
.await;
persist_skipped_local_execution_candidates_with_context(
state,
trace_id,
persistence_policy.skipped,
available_candidate_count,
skipped_candidates,
)
.await;
Ok((attempts, candidate_count))
}

View File

@@ -1,176 +0,0 @@
use aether_contracts::{ExecutionPlan, RequestBody};
use super::{
augment_sync_report_context, generic_decision_missing_exact_provider_request,
take_non_empty_string, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::transport::ensure_upstream_auth_header;
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) fn build_gemini_sync_plan_from_decision(
_parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalSyncPlanAndReport>, GatewayError> {
let mut payload = payload;
if generic_decision_missing_exact_provider_request(&payload) {
return Ok(None);
}
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let auth_header = take_non_empty_string(&mut payload.auth_header);
let auth_value = take_non_empty_string(&mut payload.auth_value);
if auth_header.is_some() != auth_value.is_some() {
return Ok(None);
}
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(provider_request_body_value) = payload.provider_request_body.take() else {
return Ok(None);
};
let mut provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
if let (Some(auth_header), Some(auth_value)) = (auth_header.as_deref(), auth_value.as_deref()) {
ensure_upstream_auth_header(&mut provider_request_headers, auth_header, auth_value);
}
if payload.upstream_is_stream {
provider_request_headers
.entry("accept".to_string())
.or_insert_with(|| "text/event-stream".to_string());
}
let content_type = payload
.content_type
.take()
.or_else(|| Some("application/json".to_string()));
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: "POST".to_string(),
url,
headers: std::mem::take(&mut provider_request_headers),
content_type,
content_encoding: None,
body: RequestBody::from_json(provider_request_body_value),
stream: payload.upstream_is_stream,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalSyncPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
pub(crate) fn build_gemini_stream_plan_from_decision(
_parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalStreamPlanAndReport>, GatewayError> {
let mut payload = payload;
if generic_decision_missing_exact_provider_request(&payload) {
return Ok(None);
}
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let auth_header = take_non_empty_string(&mut payload.auth_header);
let auth_value = take_non_empty_string(&mut payload.auth_value);
if auth_header.is_some() != auth_value.is_some() {
return Ok(None);
}
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(provider_request_body_value) = payload.provider_request_body.take() else {
return Ok(None);
};
let mut provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
if let (Some(auth_header), Some(auth_value)) = (auth_header.as_deref(), auth_value.as_deref()) {
ensure_upstream_auth_header(&mut provider_request_headers, auth_header, auth_value);
}
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
let content_type = payload
.content_type
.take()
.or_else(|| Some("application/json".to_string()));
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: "POST".to_string(),
url,
headers: std::mem::take(&mut provider_request_headers),
content_type,
content_encoding: None,
body: RequestBody::from_json(provider_request_body_value),
stream: true,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalStreamPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}

View File

@@ -1,127 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
use crate::ai_pipeline::transport::apply_local_body_rules;
use crate::ai_pipeline::transport::url::{
build_claude_messages_url, build_openai_chat_url, build_openai_responses_url,
build_passthrough_path_url,
};
use crate::ai_pipeline::{
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
build_cross_format_openai_chat_request_body as pipeline_build_cross_format_openai_chat_request_body,
build_local_openai_chat_request_body as pipeline_build_local_openai_chat_request_body,
GatewayProviderTransportSnapshot,
};
pub(crate) fn build_local_openai_chat_request_body(
body_json: &Value,
mapped_model: &str,
upstream_is_stream: bool,
body_rules: Option<&Value>,
) -> Option<Value> {
let mut provider_request_body =
pipeline_build_local_openai_chat_request_body(body_json, mapped_model, upstream_is_stream)?;
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
Some(provider_request_body)
}
pub(crate) fn build_local_openai_chat_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
) -> Option<String> {
let custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match custom_path {
Some(path) => {
build_passthrough_path_url(&transport.endpoint.base_url, path, parts.uri.query(), &[])
}
None => Some(build_openai_chat_url(
&transport.endpoint.base_url,
parts.uri.query(),
)),
}
}
pub(crate) fn build_cross_format_openai_chat_request_body(
body_json: &Value,
mapped_model: &str,
provider_type: &str,
provider_api_format: &str,
upstream_is_stream: bool,
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
) -> Option<Value> {
let mut provider_request_body = pipeline_build_cross_format_openai_chat_request_body(
body_json,
mapped_model,
provider_api_format,
upstream_is_stream,
)?;
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
pub(crate) fn build_cross_format_openai_chat_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<String> {
let conversion_kind = request_conversion_kind("openai:chat", provider_api_format)?;
let custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match custom_path {
Some(path) => {
build_passthrough_path_url(&transport.endpoint.base_url, path, parts.uri.query(), &[])
}
None => match conversion_kind {
RequestConversionKind::ToClaudeStandard => Some(build_claude_messages_url(
&transport.endpoint.base_url,
parts.uri.query(),
)),
RequestConversionKind::ToGeminiStandard => {
crate::ai_pipeline::build_provider_transport_request_url(
transport,
provider_api_format,
Some(mapped_model),
upstream_is_stream,
parts.uri.query(),
None,
)
}
RequestConversionKind::ToOpenAiResponses => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
false,
)),
_ => None,
},
}
}

View File

@@ -1,180 +0,0 @@
use std::collections::BTreeMap;
use serde_json::Value;
use url::form_urlencoded;
use crate::ai_pipeline::conversion::{request_conversion_kind, RequestConversionKind};
use crate::ai_pipeline::transport::antigravity::{
build_antigravity_v1internal_url, AntigravityRequestUrlAction,
};
use crate::ai_pipeline::transport::apply_local_body_rules;
use crate::ai_pipeline::transport::url::{
build_claude_messages_url, build_openai_chat_url, build_openai_responses_url,
build_passthrough_path_url,
};
use crate::ai_pipeline::{
apply_codex_openai_responses_special_body_edits,
apply_openai_responses_compact_special_body_edits,
build_cross_format_openai_responses_request_body as pipeline_build_cross_format_openai_responses_request_body,
build_local_openai_responses_request_body as pipeline_build_local_openai_responses_request_body,
GatewayProviderTransportSnapshot,
};
pub(crate) fn build_local_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
require_streaming: bool,
provider_type: &str,
provider_api_format: &str,
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
) -> Option<Value> {
let mut provider_request_body = pipeline_build_local_openai_responses_request_body(
body_json,
mapped_model,
require_streaming,
)?;
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
pub(crate) fn build_cross_format_openai_responses_request_body(
body_json: &Value,
mapped_model: &str,
client_api_format: &str,
provider_api_format: &str,
upstream_is_stream: bool,
provider_type: &str,
body_rules: Option<&Value>,
user_api_key_id: Option<&str>,
) -> Option<Value> {
let mut provider_request_body = pipeline_build_cross_format_openai_responses_request_body(
body_json,
mapped_model,
client_api_format,
provider_api_format,
upstream_is_stream,
)?;
if !apply_local_body_rules(&mut provider_request_body, body_rules, Some(body_json)) {
return None;
}
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
provider_type,
provider_api_format,
body_rules,
user_api_key_id,
);
apply_openai_responses_compact_special_body_edits(
&mut provider_request_body,
provider_api_format,
);
Some(provider_request_body)
}
pub(crate) fn build_local_openai_responses_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
compact: bool,
) -> Option<String> {
let custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match custom_path {
Some(path) => {
build_passthrough_path_url(&transport.endpoint.base_url, path, parts.uri.query(), &[])
}
None => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
compact,
)),
}
}
pub(crate) fn build_cross_format_openai_responses_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
client_api_format: &str,
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<String> {
let conversion_kind = request_conversion_kind(client_api_format, provider_api_format)?;
if transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("antigravity")
{
let query = parts.uri.query().map(|query| {
form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect::<BTreeMap<String, String>>()
});
return build_antigravity_v1internal_url(
&transport.endpoint.base_url,
if upstream_is_stream {
AntigravityRequestUrlAction::StreamGenerateContent
} else {
AntigravityRequestUrlAction::GenerateContent
},
query.as_ref(),
);
}
let custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match custom_path {
Some(path) => {
build_passthrough_path_url(&transport.endpoint.base_url, path, parts.uri.query(), &[])
}
None => match conversion_kind {
RequestConversionKind::ToOpenAIChat => Some(build_openai_chat_url(
&transport.endpoint.base_url,
parts.uri.query(),
)),
RequestConversionKind::ToOpenAiResponses => Some(build_openai_responses_url(
&transport.endpoint.base_url,
parts.uri.query(),
false,
)),
RequestConversionKind::ToClaudeStandard => Some(build_claude_messages_url(
&transport.endpoint.base_url,
parts.uri.query(),
)),
RequestConversionKind::ToGeminiStandard => {
crate::ai_pipeline::build_provider_transport_request_url(
transport,
provider_api_format,
Some(mapped_model),
upstream_is_stream,
parts.uri.query(),
None,
)
}
},
}
}

View File

@@ -1,102 +0,0 @@
use std::collections::BTreeSet;
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use super::super::{GatewayError, LocalOpenAiChatDecisionInput};
use crate::ai_pipeline::conversion::request_candidate_api_formats;
use crate::ai_pipeline::planner::candidate_resolution::SkippedLocalExecutionCandidate;
use crate::ai_pipeline::planner::candidate_source::auth_snapshot_allows_cross_format_candidate;
use crate::ai_pipeline::PlannerAppState;
use crate::clock::current_unix_secs;
use crate::AppState;
pub(crate) async fn list_local_openai_chat_candidates(
state: &AppState,
input: &LocalOpenAiChatDecisionInput,
require_streaming: bool,
) -> Result<
(
Vec<SchedulerMinimalCandidateSelectionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
),
GatewayError,
> {
let planner_state = PlannerAppState::new(state);
let now_unix_secs = current_unix_secs();
let mut combined = Vec::new();
let mut seen = BTreeSet::new();
let mut skipped = Vec::new();
let mut seen_skipped = BTreeSet::new();
let api_formats = request_candidate_api_formats("openai:chat", require_streaming);
for api_format in api_formats {
let auth_snapshot = if api_format == "openai:chat" {
Some(&input.auth_snapshot)
} else {
None
};
let (mut candidates, skipped_candidates) = planner_state
.list_selectable_candidates_with_skip_reasons(
api_format,
&input.requested_model,
require_streaming,
input.required_capabilities.as_ref(),
auth_snapshot,
now_unix_secs,
)
.await?;
if api_format != "openai:chat" {
candidates.retain(|candidate| {
auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
candidate,
)
});
}
for skipped_candidate in skipped_candidates {
if api_format != "openai:chat"
&& !auth_snapshot_allows_cross_format_candidate(
&input.auth_snapshot,
&input.requested_model,
&skipped_candidate.candidate,
)
{
continue;
}
let candidate_key = format!(
"{}:{}:{}:{}:{}",
skipped_candidate.candidate.provider_id,
skipped_candidate.candidate.endpoint_id,
skipped_candidate.candidate.key_id,
skipped_candidate.candidate.model_id,
skipped_candidate.candidate.selected_provider_model_name,
);
if seen_skipped.insert(candidate_key) {
skipped.push(SkippedLocalExecutionCandidate {
candidate: skipped_candidate.candidate,
skip_reason: skipped_candidate.skip_reason,
transport: None,
ranking: None,
extra_data: None,
});
}
}
for candidate in candidates {
let candidate_key = format!(
"{}:{}:{}:{}:{}",
candidate.provider_id,
candidate.endpoint_id,
candidate.key_id,
candidate.model_id,
candidate.selected_provider_model_name,
);
if seen.insert(candidate_key) {
combined.push(candidate);
}
}
}
Ok((combined, skipped))
}

View File

@@ -1,190 +0,0 @@
use aether_contracts::{ExecutionPlan, RequestBody};
use super::{
augment_sync_report_context, take_non_empty_string, LocalStreamPlanAndReport,
LocalSyncPlanAndReport,
};
use crate::ai_pipeline::contracts::generic_decision_missing_exact_provider_request;
use crate::ai_pipeline::provider_adaptation_requires_eventstream_accept;
use crate::ai_pipeline::transport::ensure_upstream_auth_header;
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) fn build_standard_sync_plan_from_decision(
_parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<LocalSyncPlanAndReport>, GatewayError> {
let mut payload = payload;
if generic_decision_missing_exact_provider_request(&payload) {
return Ok(None);
}
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let auth_header = take_non_empty_string(&mut payload.auth_header);
let auth_value = take_non_empty_string(&mut payload.auth_value);
if auth_header.is_some() != auth_value.is_some() {
return Ok(None);
}
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(provider_request_body_value) = payload.provider_request_body.take() else {
return Ok(None);
};
let mut provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
if let (Some(auth_header), Some(auth_value)) = (auth_header.as_deref(), auth_value.as_deref()) {
ensure_upstream_auth_header(&mut provider_request_headers, auth_header, auth_value);
}
if payload.upstream_is_stream {
provider_request_headers
.entry("accept".to_string())
.or_insert_with(|| "text/event-stream".to_string());
}
let content_type = payload
.content_type
.take()
.or_else(|| Some("application/json".to_string()));
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: "POST".to_string(),
url,
headers: std::mem::take(&mut provider_request_headers),
content_type,
content_encoding: None,
body: RequestBody::from_json(provider_request_body_value),
stream: payload.upstream_is_stream,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalSyncPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
pub(crate) fn build_standard_stream_plan_from_decision(
_parts: &http::request::Parts,
_body_json: &serde_json::Value,
payload: GatewayControlSyncDecisionResponse,
_inject_stream_flag: bool,
) -> Result<Option<LocalStreamPlanAndReport>, GatewayError> {
let mut payload = payload;
if generic_decision_missing_exact_provider_request(&payload) {
return Ok(None);
}
let Some(request_id) = take_non_empty_string(&mut payload.request_id) else {
return Ok(None);
};
let Some(provider_id) = take_non_empty_string(&mut payload.provider_id) else {
return Ok(None);
};
let Some(endpoint_id) = take_non_empty_string(&mut payload.endpoint_id) else {
return Ok(None);
};
let Some(key_id) = take_non_empty_string(&mut payload.key_id) else {
return Ok(None);
};
let Some(url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let auth_header = take_non_empty_string(&mut payload.auth_header);
let auth_value = take_non_empty_string(&mut payload.auth_value);
if auth_header.is_some() != auth_value.is_some() {
return Ok(None);
}
let Some(provider_api_format) = take_non_empty_string(&mut payload.provider_api_format) else {
return Ok(None);
};
let Some(client_api_format) = take_non_empty_string(&mut payload.client_api_format) else {
return Ok(None);
};
let Some(provider_request_body_value) = payload.provider_request_body.take() else {
return Ok(None);
};
let envelope_name = payload
.report_context
.as_ref()
.and_then(|context| context.get("envelope_name"))
.and_then(serde_json::Value::as_str);
let mut provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
if let (Some(auth_header), Some(auth_value)) = (auth_header.as_deref(), auth_value.as_deref()) {
ensure_upstream_auth_header(&mut provider_request_headers, auth_header, auth_value);
}
if provider_adaptation_requires_eventstream_accept(envelope_name, provider_api_format.as_str())
{
provider_request_headers
.entry("accept".to_string())
.or_insert_with(|| "application/vnd.amazon.eventstream".to_string());
} else {
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
}
let content_type = payload
.content_type
.take()
.or_else(|| Some("application/json".to_string()));
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&provider_request_body_value,
)?;
let plan = ExecutionPlan {
request_id,
candidate_id: payload.candidate_id.take(),
provider_name: payload.provider_name.take(),
provider_id,
endpoint_id,
key_id,
method: "POST".to_string(),
url,
headers: std::mem::take(&mut provider_request_headers),
content_type,
content_encoding: None,
body: RequestBody::from_json(provider_request_body_value),
stream: true,
client_api_format,
provider_api_format,
model_name: payload.model_name.take(),
proxy: payload.proxy.take(),
tls_profile: payload.tls_profile.take(),
timeouts: payload.timeouts.take(),
};
Ok(Some(LocalStreamPlanAndReport {
plan,
report_kind: payload.report_kind,
report_context,
}))
}

View File

@@ -1,749 +0,0 @@
use serde_json::Value;
use crate::ai_pipeline::planner::{CandidateFailureDiagnostic, CandidateFailureDiagnosticKind};
pub(crate) fn request_body_build_failure_extra_data(
body_json: &Value,
client_api_format: &str,
provider_api_format: &str,
) -> Option<Value> {
let diagnostic =
diagnose_request_body_build_failure(body_json, client_api_format, provider_api_format)?;
Some(
diagnostic
.formats(client_api_format, provider_api_format)
.source(request_body_build_source(
client_api_format,
provider_api_format,
))
.to_extra_data(),
)
}
pub(crate) fn same_format_provider_request_body_failure_extra_data(
body_json: &Value,
provider_api_format: &str,
body_rules: Option<&Value>,
context: &str,
) -> Option<Value> {
let diagnostic =
diagnose_same_format_provider_request_body_failure(body_json, body_rules, context)?;
Some(
diagnostic
.formats(provider_api_format, provider_api_format)
.source(context)
.to_extra_data(),
)
}
type RequestBodyBuildDiagnostic = CandidateFailureDiagnostic;
fn diagnose_request_body_build_failure(
body_json: &Value,
client_api_format: &str,
provider_api_format: &str,
) -> Option<RequestBodyBuildDiagnostic> {
if !body_json.is_object() {
return Some(diagnostic("$", "请求体必须是 JSON object"));
}
if is_openai_responses_client_format(client_api_format) {
if let Some(diagnostic) = diagnose_openai_responses_request(body_json) {
return Some(diagnostic);
}
return Some(diagnostic(
"$",
"OpenAI Responses 请求体初步结构检查通过;失败可能发生在后续跨格式转换或 Body 规则应用",
));
}
if client_api_format == "openai:chat"
&& (provider_api_format.starts_with("claude:")
|| provider_api_format.starts_with("gemini:"))
{
return diagnose_openai_chat_cross_format_request(body_json, provider_api_format);
}
Some(diagnostic(
"$",
"请求体转换失败;当前转换器未返回更细的字段路径",
))
}
fn is_openai_responses_client_format(client_api_format: &str) -> bool {
crate::ai_pipeline::is_openai_responses_family_format(client_api_format)
}
fn diagnose_same_format_provider_request_body_failure(
body_json: &Value,
body_rules: Option<&Value>,
context: &str,
) -> Option<RequestBodyBuildDiagnostic> {
if !body_json.is_object() {
return Some(diagnostic("$", "反代请求体必须是 JSON object"));
}
if body_rules.is_some_and(|rules| !rules.is_array()) {
return Some(diagnostic(
"$.endpoint.body_rules",
"Endpoint Body 规则必须是数组,本地反代无法应用该配置",
));
}
match context {
"kiro_envelope" => Some(diagnostic(
"$",
"Kiro 反代请求体包装失败;请检查 Kiro auth_config 与 Endpoint Body 规则",
)),
"antigravity_envelope" => Some(diagnostic(
"$",
"Antigravity 反代请求体包装失败;请检查请求体是否满足该传输封装要求",
)),
_ => Some(diagnostic(
"$",
"反代请求体构建失败;当前路径未返回更细的字段信息",
)),
}
}
fn diagnose_openai_chat_cross_format_request(
body_json: &Value,
provider_api_format: &str,
) -> Option<RequestBodyBuildDiagnostic> {
let request = body_json.as_object()?;
if let Some(messages) = request.get("messages") {
let Some(messages) = messages.as_array() else {
return Some(diagnostic(
"$.messages",
"OpenAI Chat 的 messages 必须是数组",
));
};
for (message_index, message) in messages.iter().enumerate() {
let Some(message_object) = message.as_object() else {
return Some(diagnostic(
format!("$.messages[{message_index}]"),
"message 必须是 object",
));
};
let role = message_object
.get("role")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
match role.as_str() {
"system" | "developer" => {
if let Some(diagnostic) = diagnose_openai_text_content(
message_object.get("content"),
format!("$.messages[{message_index}].content"),
) {
return Some(diagnostic);
}
}
"user" | "assistant" => {
if let Some(diagnostic) = diagnose_openai_content_blocks(
message_object.get("content"),
format!("$.messages[{message_index}].content"),
role.as_str(),
) {
return Some(diagnostic);
}
if role == "assistant" {
if let Some(diagnostic) = diagnose_openai_assistant_tool_calls(
message_object.get("tool_calls"),
format!("$.messages[{message_index}].tool_calls"),
) {
return Some(diagnostic);
}
}
}
"tool" => {
let valid_tool_call_id = message_object
.get("tool_call_id")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if !valid_tool_call_id {
return Some(diagnostic(
format!("$.messages[{message_index}].tool_call_id"),
"tool 消息必须包含非空 tool_call_id",
));
}
}
_ => {}
}
}
}
if let Some(diagnostic) = diagnose_openai_tools(request.get("tools"), provider_api_format) {
return Some(diagnostic);
}
diagnose_openai_tool_choice(request.get("tool_choice"))
}
fn diagnose_openai_responses_request(body_json: &Value) -> Option<RequestBodyBuildDiagnostic> {
let request = body_json.as_object()?;
if let Some(diagnostic) = diagnose_openai_responses_text_content(
request.get("instructions"),
"$.instructions".to_string(),
) {
return Some(diagnostic);
}
if let Some(diagnostic) = diagnose_openai_responses_input(request.get("input")) {
return Some(diagnostic);
}
if let Some(diagnostic) = diagnose_openai_responses_tools(request.get("tools")) {
return Some(diagnostic);
}
diagnose_openai_responses_tool_choice(request.get("tool_choice"))
}
fn diagnose_openai_responses_input(input: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
let Some(input) = input else {
return None;
};
match input {
Value::Null | Value::String(_) => None,
Value::Array(items) => {
for (item_index, item) in items.iter().enumerate() {
if item.is_string() {
continue;
}
let item_path = format!("$.input[{item_index}]");
let Some(item_object) = item.as_object() else {
return Some(diagnostic(
item_path,
"OpenAI Responses input 数组项必须是 string 或 object",
));
};
let item_type = item_object
.get("type")
.and_then(Value::as_str)
.unwrap_or("message")
.trim()
.to_ascii_lowercase();
match item_type.as_str() {
"message" => {
let role = item_object
.get("role")
.and_then(Value::as_str)
.unwrap_or("user")
.trim()
.to_ascii_lowercase();
if role == "system" || role == "developer" {
if let Some(diagnostic) = diagnose_openai_responses_text_content(
item_object.get("content"),
format!("{item_path}.content"),
) {
return Some(diagnostic);
}
} else if let Some(diagnostic) = diagnose_openai_responses_message_content(
item_object.get("content"),
format!("{item_path}.content"),
) {
return Some(diagnostic);
}
}
"function_call" => {
let valid_name = item_object
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if !valid_name {
return Some(diagnostic(
format!("{item_path}.name"),
"function_call 必须包含非空 name",
));
}
}
_ => {}
}
}
None
}
_ => Some(diagnostic(
"$.input",
"OpenAI Responses input 必须是 string、array 或 null",
)),
}
}
fn diagnose_openai_responses_text_content(
content: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
match content {
None | Some(Value::Null) | Some(Value::String(_)) => None,
Some(Value::Array(parts)) => {
for (part_index, part) in parts.iter().enumerate() {
if !part.is_object() {
return Some(diagnostic(
format!("{path}[{part_index}]"),
"文本 content 数组项必须是 object",
));
}
}
None
}
Some(_) => Some(diagnostic(
path,
"文本 content 必须是 string、array 或 null",
)),
}
}
fn diagnose_openai_responses_message_content(
content: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
match content {
None | Some(Value::Null) | Some(Value::String(_)) => None,
Some(Value::Array(parts)) => {
for (part_index, part) in parts.iter().enumerate() {
let part_path = format!("{path}[{part_index}]");
let Some(part_object) = part.as_object() else {
return Some(diagnostic(part_path, "message content 数组项必须是 object"));
};
let part_type = part_object
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
if matches!(
part_type.as_str(),
"input_image" | "output_image" | "image_url"
) && image_part_url(part_object).is_none()
{
return Some(diagnostic(
part_path,
"图片 content 缺少 image_url/url无法规范化为 OpenAI Chat 图片内容",
));
}
}
None
}
Some(_) => None,
}
}
fn diagnose_openai_text_content(
content: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
match content {
None | Some(Value::Null) | Some(Value::String(_)) => None,
Some(Value::Array(parts)) => {
for (part_index, part) in parts.iter().enumerate() {
if !part.is_object() {
return Some(diagnostic(
format!("{path}[{part_index}]"),
"content 数组项必须是 object",
));
}
}
None
}
Some(_) => Some(diagnostic(path, "content 必须是 string、array 或 null")),
}
}
fn diagnose_openai_content_blocks(
content: Option<&Value>,
path: String,
role: &str,
) -> Option<RequestBodyBuildDiagnostic> {
match content {
None | Some(Value::Null) | Some(Value::String(_)) => None,
Some(Value::Array(parts)) => {
for (part_index, part) in parts.iter().enumerate() {
let part_path = format!("{path}[{part_index}]");
let Some(part_object) = part.as_object() else {
return Some(diagnostic(part_path, "content 数组项必须是 object"));
};
let part_type = part_object
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
if matches!(part_type, "image_url" | "input_image" | "output_image")
&& role == "user"
&& image_part_url(part_object).is_none()
{
return Some(diagnostic(
part_path,
"图片 content 缺少 image_url/url无法转换为 Claude image block",
));
}
}
None
}
Some(_) => Some(diagnostic(path, "content 必须是 string、array 或 null")),
}
}
fn diagnose_openai_assistant_tool_calls(
tool_calls: Option<&Value>,
path: String,
) -> Option<RequestBodyBuildDiagnostic> {
let Some(tool_calls) = tool_calls else {
return None;
};
let Some(tool_calls) = tool_calls.as_array() else {
return Some(diagnostic(path, "assistant.tool_calls 必须是数组"));
};
for (tool_call_index, tool_call) in tool_calls.iter().enumerate() {
let tool_call_path = format!("{path}[{tool_call_index}]");
let Some(tool_call_object) = tool_call.as_object() else {
return Some(diagnostic(tool_call_path, "tool_call 必须是 object"));
};
let Some(function) = tool_call_object.get("function").and_then(Value::as_object) else {
return Some(diagnostic(
format!("{tool_call_path}.function"),
"tool_call 必须包含 function object",
));
};
let valid_name = function
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if !valid_name {
return Some(diagnostic(
format!("{tool_call_path}.function.name"),
"tool_call.function.name 必须是非空字符串",
));
}
}
None
}
fn diagnose_openai_tools(
tools: Option<&Value>,
provider_api_format: &str,
) -> Option<RequestBodyBuildDiagnostic> {
let Some(tools) = tools else {
return None;
};
let Some(tools) = tools.as_array() else {
return Some(diagnostic("$.tools", "OpenAI Chat 的 tools 必须是数组"));
};
for (tool_index, tool) in tools.iter().enumerate() {
let tool_path = format!("$.tools[{tool_index}]");
let Some(tool_object) = tool.as_object() else {
return Some(diagnostic(tool_path, "tool 必须是 object"));
};
if tool_object
.get("type")
.and_then(Value::as_str)
.is_some_and(|value| value != "function")
{
continue;
}
let Some(function) = tool_object.get("function").and_then(Value::as_object) else {
let native_tool_hint = if provider_api_format.starts_with("claude:") {
";如果这是 Claude 原生 tool请改为 OpenAI function tool 格式"
} else if provider_api_format.starts_with("gemini:") {
";如果这是 Gemini 原生 tool请改为 OpenAI function tool 格式"
} else {
""
};
return Some(diagnostic(
format!("{tool_path}.function"),
format!("OpenAI tool 必须包含 function object{native_tool_hint}"),
));
};
let valid_name = function
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if !valid_name {
return Some(diagnostic(
format!("{tool_path}.function.name"),
"OpenAI tool 的 function.name 必须是非空字符串",
));
}
}
None
}
fn diagnose_openai_responses_tools(tools: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
let Some(tools) = tools else {
return None;
};
let Some(tool_values) = tools.as_array() else {
return None;
};
for (tool_index, tool) in tool_values.iter().enumerate() {
let tool_path = format!("$.tools[{tool_index}]");
let Some(tool_object) = tool.as_object() else {
return Some(diagnostic(tool_path, "OpenAI Responses tool 必须是 object"));
};
let tool_type = tool_object
.get("type")
.and_then(Value::as_str)
.unwrap_or("function")
.trim()
.to_ascii_lowercase();
if tool_type.starts_with("web_search")
|| tool_object.get("function").is_some()
|| tool_type != "function"
{
continue;
}
let valid_name = tool_object
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if !valid_name {
return Some(diagnostic(
format!("{tool_path}.name"),
"OpenAI Responses function tool 必须包含非空 name",
));
}
}
None
}
fn diagnose_openai_responses_tool_choice(
tool_choice: Option<&Value>,
) -> Option<RequestBodyBuildDiagnostic> {
let Some(Value::Object(object)) = tool_choice else {
return None;
};
let is_cli_function_choice = object.get("function").is_none()
&& object
.get("type")
.and_then(Value::as_str)
.is_some_and(|value| value.eq_ignore_ascii_case("function"));
if !is_cli_function_choice {
return None;
}
let valid_name = object
.get("name")
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if valid_name {
None
} else {
Some(diagnostic(
"$.tool_choice.name",
"OpenAI Responses tool_choice 指定 function 时必须包含非空 name",
))
}
}
fn diagnose_openai_tool_choice(tool_choice: Option<&Value>) -> Option<RequestBodyBuildDiagnostic> {
let Some(Value::Object(object)) = tool_choice else {
return None;
};
let valid_name = object
.get("function")
.and_then(Value::as_object)
.and_then(|function| function.get("name"))
.and_then(Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if valid_name {
None
} else {
Some(diagnostic(
"$.tool_choice.function.name",
"tool_choice 指定具体工具时必须包含非空 function.name",
))
}
}
fn image_part_url(part_object: &serde_json::Map<String, Value>) -> Option<&str> {
part_object
.get("image_url")
.and_then(|value| {
value.as_str().or_else(|| {
value
.as_object()
.and_then(|object| object.get("url"))
.and_then(Value::as_str)
})
})
.or_else(|| part_object.get("url").and_then(Value::as_str))
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn diagnostic(path: impl Into<String>, message: impl Into<String>) -> RequestBodyBuildDiagnostic {
CandidateFailureDiagnostic::new(
CandidateFailureDiagnosticKind::RequestBodyBuild,
path,
message,
)
}
fn request_body_build_source(client_api_format: &str, provider_api_format: &str) -> String {
format!("{client_api_format}_to_{provider_api_format}")
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::request_body_build_failure_extra_data;
#[test]
fn openai_chat_to_claude_reports_claude_native_tool_shape() {
let body = json!({
"model": "gpt-5.4",
"messages": [{ "role": "user", "content": "hello" }],
"tools": [{
"name": "read_file",
"description": "Read a file",
"input_schema": { "type": "object" }
}]
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "claude:messages")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.tools[0].function"
);
assert_eq!(
diagnostic["failure_diagnostic"]["kind"],
"request_body_build"
);
assert_eq!(
diagnostic["failure_diagnostic"]["source"],
"openai:chat_to_claude:messages"
);
assert!(diagnostic["request_body_build_error"]["message"]
.as_str()
.expect("message")
.contains("Claude 原生 tool"));
}
#[test]
fn openai_chat_to_claude_reports_invalid_message_content_part() {
let body = json!({
"model": "gpt-5.4",
"messages": [{
"role": "user",
"content": ["not-an-object"]
}]
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "claude:messages")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.messages[0].content[0]"
);
}
#[test]
fn openai_chat_to_gemini_reports_gemini_native_tool_shape() {
let body = json!({
"model": "gpt-5.4",
"messages": [{ "role": "user", "content": "hello" }],
"tools": [{
"functionDeclarations": [{
"name": "search",
"parameters": { "type": "object" }
}]
}]
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:chat", "gemini:generate_content")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.tools[0].function"
);
assert!(diagnostic["request_body_build_error"]["message"]
.as_str()
.expect("message")
.contains("Gemini 原生 tool"));
}
#[test]
fn openai_responses_reports_invalid_function_call_name() {
let body = json!({
"model": "gpt-5.4",
"input": [{
"type": "function_call",
"arguments": "{}"
}]
});
let diagnostic =
request_body_build_failure_extra_data(&body, "openai:responses", "claude:messages")
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.input[0].name"
);
}
#[test]
fn openai_responses_reports_invalid_tool_choice_name() {
let body = json!({
"model": "gpt-5.4",
"input": "hello",
"tool_choice": { "type": "function" }
});
let diagnostic = request_body_build_failure_extra_data(
&body,
"openai:responses",
"gemini:generate_content",
)
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.tool_choice.name"
);
}
#[test]
fn same_format_provider_reports_non_object_body() {
let diagnostic = super::same_format_provider_request_body_failure_extra_data(
&json!("raw"),
"openai:chat",
None,
"same_format",
)
.expect("diagnostic");
assert_eq!(diagnostic["request_body_build_error"]["path"], "$");
assert!(diagnostic["request_body_build_error"]["message"]
.as_str()
.expect("message")
.contains("反代请求体必须是 JSON object"));
}
#[test]
fn same_format_provider_reports_invalid_body_rules_shape() {
let diagnostic = super::same_format_provider_request_body_failure_extra_data(
&json!({ "model": "gpt-5.4" }),
"openai:chat",
Some(&json!({ "action": "set" })),
"same_format",
)
.expect("diagnostic");
assert_eq!(
diagnostic["request_body_build_error"]["path"],
"$.endpoint.body_rules"
);
}
}

View File

@@ -1,57 +0,0 @@
pub(crate) mod antigravity {
pub(crate) use aether_ai_pipeline::transport::antigravity::*;
}
pub(crate) mod auth {
pub(crate) use aether_ai_pipeline::transport::auth::*;
}
pub(crate) mod claude_code {
pub(crate) use aether_ai_pipeline::transport::claude_code::*;
}
pub(crate) mod kiro {
pub(crate) use aether_ai_pipeline::transport::kiro::*;
}
pub(crate) mod oauth_refresh {
pub(crate) use aether_ai_pipeline::transport::oauth_refresh::*;
}
pub(crate) mod policy {
pub(crate) use aether_ai_pipeline::transport::policy::*;
}
pub(crate) mod provider_types {
pub(crate) use aether_ai_pipeline::transport::provider_types::*;
}
pub(crate) mod rules {
pub(crate) use aether_ai_pipeline::transport::rules::*;
}
pub(crate) mod snapshot {
pub(crate) use aether_ai_pipeline::transport::snapshot::*;
}
pub(crate) mod url {
pub(crate) use aether_ai_pipeline::transport::url::*;
}
pub(crate) mod vertex {
pub(crate) use aether_ai_pipeline::transport::vertex::*;
}
pub(crate) use aether_ai_pipeline::transport::{
apply_local_body_rules, apply_local_header_rules, body_rules_are_locally_supported,
body_rules_handle_path, build_passthrough_headers, ensure_upstream_auth_header,
header_rules_are_locally_supported, local_gemini_transport_unsupported_reason_with_network,
local_openai_chat_transport_unsupported_reason,
local_standard_transport_unsupported_reason_with_network, resolve_transport_execution_timeouts,
resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity,
resolve_transport_tls_profile, should_skip_upstream_passthrough_header,
supports_local_gemini_transport_with_network,
supports_local_generic_oauth_request_auth_resolution,
supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported,
GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth,
};

View File

@@ -1,7 +1,10 @@
pub(crate) mod kiro;
pub(crate) mod private_envelope;
pub(crate) use crate::ai_pipeline::{
pub(crate) mod kiro {
pub(crate) use crate::ai_serving::pure::KiroToClaudeCliStreamState;
}
pub(crate) use crate::ai_serving::{
provider_adaptation_allows_sync_finalize_envelope, provider_adaptation_anchor_api_format,
provider_adaptation_descriptor_for_envelope, provider_adaptation_descriptor_for_provider_type,
provider_adaptation_requires_eventstream_accept,

View File

@@ -0,0 +1,10 @@
#[path = "private_envelope/sync.rs"]
mod sync;
pub(crate) use self::sync::maybe_normalize_provider_private_sync_report_payload;
pub(crate) use crate::ai_serving::{
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
normalize_provider_private_response_value, provider_private_response_allows_sync_finalize,
stream_body_contains_error_event, transform_provider_private_stream_line,
ProviderPrivateStreamNormalizer,
};

View File

@@ -3,11 +3,10 @@ use serde_json::Value;
use crate::{usage::GatewaySyncReportRequest, GatewayError};
use super::stream::maybe_build_provider_private_stream_normalizer;
use super::{
normalize_provider_private_report_context, normalize_provider_private_response_value,
provider_private_response_allows_sync_finalize, stream_body_contains_error_event,
ProviderPrivateStreamNormalizer,
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
normalize_provider_private_response_value, provider_private_response_allows_sync_finalize,
stream_body_contains_error_event, ProviderPrivateStreamNormalizer,
};
pub(crate) fn maybe_normalize_provider_private_sync_report_payload(
@@ -65,7 +64,7 @@ fn normalize_provider_private_stream_bytes(
else {
return Ok(Some(body.to_vec()));
};
let mut normalized = normalizer.push_chunk(body)?;
normalized.extend(normalizer.finish()?);
let mut normalized = normalizer.push_chunk(body).map_err(GatewayError::from)?;
normalized.extend(normalizer.finish().map_err(GatewayError::from)?);
Ok(Some(normalized))
}

View File

@@ -1,6 +1,6 @@
use crate::ai_pipeline::{is_json_request, GatewayControlDecision};
use crate::ai_serving::{is_json_request, GatewayControlDecision};
pub(crate) use crate::ai_pipeline::{
pub(crate) use crate::ai_serving::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_local_gemini_files_stream_plan_and_reports_for_kind,
build_local_gemini_files_sync_plan_and_reports_for_kind,
@@ -20,12 +20,15 @@ pub(crate) use crate::ai_pipeline::{
maybe_build_sync_decision_payload, maybe_build_sync_plan_payload,
set_local_openai_chat_execution_exhausted_diagnostic,
};
pub(crate) use crate::ai_pipeline::{
pub(crate) use crate::ai_serving::{
maybe_bridge_standard_sync_json_to_stream, maybe_build_provider_private_stream_normalizer,
maybe_build_stream_response_rewriter, maybe_build_sync_finalize_outcome,
maybe_compile_sync_finalize_response, LocalCoreSyncFinalizeOutcome,
};
pub(crate) use aether_ai_pipeline::api::{
pub(crate) use crate::ai_serving::{
AiExecutionDecision, AiExecutionPlanPayload, AiStreamAttempt, AiSyncAttempt,
};
pub(crate) use aether_ai_surfaces::api::{
build_core_error_body_for_client_format, core_error_background_report_kind,
core_error_default_client_api_format, core_success_background_report_kind,
encode_kiro_sse_events, implicit_sync_finalize_report_kind, is_core_error_finalize_kind,
@@ -34,10 +37,9 @@ pub(crate) use aether_ai_pipeline::api::{
resolve_claude_sync_spec, resolve_gemini_stream_spec, resolve_gemini_sync_spec,
resolve_local_image_stream_spec, resolve_local_image_sync_spec,
resolve_local_same_format_stream_spec, resolve_local_same_format_sync_spec,
ExecutionRuntimeAuthContext, GatewayControlPlanRequest, GatewayControlPlanResponse,
GatewayControlSyncDecisionResponse, LocalCoreSyncErrorKind, LocalOpenAiImageSpec,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec, LocalStandardSourceFamily,
LocalStandardSourceMode, LocalStandardSpec, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
ExecutionRuntimeAuthContext, GatewayControlPlanRequest, LocalCoreSyncErrorKind,
LocalOpenAiImageSpec, LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
LocalStandardSourceFamily, LocalStandardSourceMode, LocalStandardSpec,
StreamingStandardTerminalObserver, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_FILES_DOWNLOAD_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
@@ -50,7 +52,7 @@ pub(crate) fn parse_direct_request_body(
parts: &http::request::Parts,
body_bytes: &axum::body::Bytes,
) -> Option<(serde_json::Value, Option<String>)> {
aether_ai_pipeline::api::parse_direct_request_body(
aether_ai_surfaces::api::parse_direct_request_body(
is_json_request(&parts.headers),
body_bytes.as_ref(),
)
@@ -60,7 +62,7 @@ pub(crate) fn resolve_execution_runtime_stream_plan_kind(
parts: &http::request::Parts,
decision: &GatewayControlDecision,
) -> Option<&'static str> {
aether_ai_pipeline::api::resolve_execution_runtime_stream_plan_kind(
aether_ai_surfaces::api::resolve_execution_runtime_stream_plan_kind(
decision.route_class.as_deref(),
decision.route_family.as_deref(),
decision.route_kind.as_deref(),
@@ -73,7 +75,7 @@ pub(crate) fn resolve_execution_runtime_sync_plan_kind(
parts: &http::request::Parts,
decision: &GatewayControlDecision,
) -> Option<&'static str> {
aether_ai_pipeline::api::resolve_execution_runtime_sync_plan_kind(
aether_ai_surfaces::api::resolve_execution_runtime_sync_plan_kind(
decision.route_class.as_deref(),
decision.route_family.as_deref(),
decision.route_kind.as_deref(),
@@ -88,31 +90,31 @@ pub(crate) fn is_matching_stream_request(
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> bool {
crate::ai_pipeline::planner_is_matching_stream_request(plan_kind, parts, body_json, body_base64)
crate::ai_serving::planner_is_matching_stream_request(plan_kind, parts, body_json, body_base64)
}
pub(crate) fn supports_sync_scheduler_decision_kind(plan_kind: &str) -> bool {
aether_ai_pipeline::api::supports_sync_scheduler_decision_kind(plan_kind)
aether_ai_surfaces::api::supports_sync_scheduler_decision_kind(plan_kind)
}
pub(crate) fn supports_stream_scheduler_decision_kind(plan_kind: &str) -> bool {
aether_ai_pipeline::api::supports_stream_scheduler_decision_kind(plan_kind)
aether_ai_surfaces::api::supports_stream_scheduler_decision_kind(plan_kind)
}
pub(crate) fn aggregate_openai_chat_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_openai_chat_stream_sync_response(body)
aether_ai_surfaces::api::aggregate_openai_chat_stream_sync_response(body)
}
pub(crate) fn aggregate_openai_responses_stream_sync_response(
body: &[u8],
) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_openai_responses_stream_sync_response(body)
aether_ai_surfaces::api::aggregate_openai_responses_stream_sync_response(body)
}
pub(crate) fn aggregate_claude_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_claude_stream_sync_response(body)
aether_ai_surfaces::api::aggregate_claude_stream_sync_response(body)
}
pub(crate) fn aggregate_gemini_stream_sync_response(body: &[u8]) -> Option<serde_json::Value> {
aether_ai_pipeline::api::aggregate_gemini_stream_sync_response(body)
aether_ai_surfaces::api::aggregate_gemini_stream_sync_response(body)
}

View File

@@ -4,17 +4,17 @@ use axum::body::Body;
use axum::http::Response;
use serde_json::Value;
use crate::ai_pipeline::{
pub(crate) use crate::ai_serving::api::{
normalize_provider_private_response_value as unwrap_local_finalize_response_value,
provider_private_response_allows_sync_finalize as local_finalize_allows_envelope,
};
use crate::ai_serving::{
build_generated_tool_call_id,
build_local_success_background_report as build_local_success_background_report_impl,
build_local_success_conversion_background_report as build_local_success_conversion_background_report_impl,
canonicalize_tool_arguments,
prepare_local_success_response_parts as prepare_local_success_response_parts_impl,
GatewayControlDecision,
};
pub(crate) use crate::ai_pipeline_api::{
normalize_provider_private_response_value as unwrap_local_finalize_response_value,
provider_private_response_allows_sync_finalize as local_finalize_allows_envelope,
GatewayControlDecision, LocalSyncReportParts,
};
use crate::api::response::build_client_response_from_parts;
use crate::{usage::GatewaySyncReportRequest, GatewayError};
@@ -40,6 +40,36 @@ fn build_local_success_response(
)
}
fn surface_report_parts_from_gateway(payload: &GatewaySyncReportRequest) -> LocalSyncReportParts {
LocalSyncReportParts {
trace_id: payload.trace_id.clone(),
report_kind: payload.report_kind.clone(),
report_context: payload.report_context.clone(),
status_code: payload.status_code,
headers: payload.headers.clone(),
body_json: payload.body_json.clone(),
client_body_json: payload.client_body_json.clone(),
body_base64: payload.body_base64.clone(),
}
}
fn gateway_report_from_surface(
source: &GatewaySyncReportRequest,
report: LocalSyncReportParts,
) -> GatewaySyncReportRequest {
GatewaySyncReportRequest {
trace_id: report.trace_id,
report_kind: report.report_kind,
report_context: report.report_context,
status_code: report.status_code,
headers: report.headers,
body_json: report.body_json,
client_body_json: report.client_body_json,
body_base64: report.body_base64,
telemetry: source.telemetry.clone(),
}
}
pub(crate) fn build_local_success_outcome(
trace_id: &str,
decision: &GatewayControlDecision,
@@ -50,8 +80,10 @@ pub(crate) fn build_local_success_outcome(
let (body_bytes, response_headers) =
prepare_local_success_response_parts_impl(&payload.headers, &body_json)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let surface_payload = surface_report_parts_from_gateway(payload);
let background_report =
build_local_success_background_report_impl(payload, body_json, report_headers);
build_local_success_background_report_impl(&surface_payload, body_json, report_headers)
.map(|report| gateway_report_from_surface(payload, report));
build_local_success_outcome_with_report(
trace_id,
decision,
@@ -88,11 +120,13 @@ pub(crate) fn build_local_success_outcome_with_conversion_report(
let (body_bytes, response_headers) =
prepare_local_success_response_parts_impl(&payload.headers, &client_body_json)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let surface_payload = surface_report_parts_from_gateway(payload);
let report_payload = build_local_success_conversion_background_report_impl(
payload,
&surface_payload,
client_body_json,
provider_body_json,
);
)
.map(|report| gateway_report_from_surface(payload, report));
build_local_success_outcome_with_report(
trace_id,

View File

@@ -2,7 +2,7 @@ use axum::body::Body;
use axum::http::Response;
use serde_json::Value;
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_serving::GatewayControlDecision;
use crate::{usage::GatewaySyncReportRequest, GatewayError};
#[path = "stream_rewrite.rs"]

View File

@@ -0,0 +1,35 @@
use serde_json::Value;
use crate::ai_serving::{
maybe_build_ai_surface_stream_rewriter, AiSurfaceFinalizeError, AiSurfaceStreamRewriter,
};
use crate::GatewayError;
pub(crate) struct LocalStreamRewriter<'a> {
inner: AiSurfaceStreamRewriter<'a>,
}
pub(crate) fn maybe_build_local_stream_rewriter<'a>(
report_context: Option<&'a Value>,
) -> Option<LocalStreamRewriter<'a>> {
maybe_build_ai_surface_stream_rewriter(report_context)
.map(|inner| LocalStreamRewriter { inner })
}
impl LocalStreamRewriter<'_> {
pub(crate) fn push_chunk(&mut self, chunk: &[u8]) -> Result<Vec<u8>, GatewayError> {
self.inner.push_chunk(chunk).map_err(map_surface_error)
}
pub(crate) fn finish(&mut self) -> Result<Vec<u8>, GatewayError> {
self.inner.finish().map_err(map_surface_error)
}
}
fn map_surface_error(error: AiSurfaceFinalizeError) -> GatewayError {
error.into()
}
#[cfg(test)]
#[path = "../tests_stream.rs"]
mod tests;

View File

@@ -0,0 +1,113 @@
use crate::ai_serving::GatewayControlDecision;
use crate::ai_serving::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::{usage::GatewaySyncReportRequest, GatewayError};
pub(crate) use crate::ai_serving::finalize::common::{
build_local_success_outcome, build_local_success_outcome_with_conversion_report,
local_finalize_allows_envelope, unwrap_local_finalize_response_value,
LocalCoreSyncFinalizeOutcome,
};
pub(crate) use crate::ai_serving::finalize::standard::{
maybe_build_standard_sync_finalize_product_from_normalized_payload,
StandardSyncFinalizeNormalizedProduct,
};
pub(crate) use crate::ai_serving::{
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
maybe_build_openai_image_sync_finalize_product,
};
pub(crate) use crate::ai_serving::{
convert_claude_chat_response_to_openai_chat, convert_claude_response_to_openai_responses,
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
};
pub(crate) fn maybe_build_local_core_sync_finalize_response(
trace_id: &str,
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
if let Some(outcome) =
maybe_build_local_openai_image_sync_finalize_response(trace_id, decision, payload)?
{
return Ok(Some(outcome));
}
let Some(normalized_payload) =
crate::ai_serving::adaptation::private_envelope::maybe_normalize_provider_private_sync_report_payload(payload)?
else {
return Ok(None);
};
let payload = &normalized_payload;
let Some(report_context) = payload.report_context.as_ref() else {
return Ok(None);
};
if !local_finalize_allows_envelope(report_context) {
return Ok(None);
}
let Some(product) = maybe_build_standard_sync_finalize_product_from_normalized_payload(
payload.report_kind.as_str(),
payload.status_code,
Some(report_context),
payload.body_json.as_ref(),
payload.body_base64.as_deref(),
)
.map_err(GatewayError::from)?
else {
return Ok(None);
};
match product {
StandardSyncFinalizeNormalizedProduct::SuccessBody(body_json) => {
let Some(body_json) = unwrap_local_finalize_response_value(body_json, report_context)
else {
return Ok(None);
};
Ok(Some(build_local_success_outcome(
trace_id, decision, payload, body_json,
)?))
}
StandardSyncFinalizeNormalizedProduct::CrossFormat(product) => {
let Some(provider_body_json) =
unwrap_local_finalize_response_value(product.provider_body_json, report_context)
else {
return Ok(None);
};
Ok(Some(build_local_success_outcome_with_conversion_report(
trace_id,
decision,
payload,
product.client_body_json,
provider_body_json,
)?))
}
}
}
fn maybe_build_local_openai_image_sync_finalize_response(
trace_id: &str,
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<LocalCoreSyncFinalizeOutcome>, GatewayError> {
let Some(product) = maybe_build_openai_image_sync_finalize_product(
payload.report_kind.as_str(),
payload.status_code,
payload.report_context.as_ref(),
payload.body_base64.as_deref(),
)
.map_err(GatewayError::from)?
else {
return Ok(None);
};
Ok(Some(build_local_success_outcome_with_conversion_report(
trace_id,
decision,
payload,
product.client_body_json,
product.provider_body_json,
)?))
}
#[cfg(test)]
#[path = "../tests_sync.rs"]
mod tests;

View File

@@ -0,0 +1,20 @@
use serde_json::Value;
use crate::GatewayError;
pub(crate) use crate::ai_serving::pure::SyncToStreamBridgeOutcome;
pub(crate) fn maybe_bridge_standard_sync_json_to_stream(
provider_body_json: &Value,
provider_api_format: &str,
client_api_format: &str,
report_context: Option<&Value>,
) -> Result<Option<SyncToStreamBridgeOutcome>, GatewayError> {
crate::ai_serving::pure::maybe_bridge_standard_sync_json_to_stream(
provider_body_json,
provider_api_format,
client_api_format,
report_context,
)
.map_err(GatewayError::from)
}

View File

@@ -1,12 +1,12 @@
use serde_json::Value;
use crate::ai_pipeline::{
use crate::ai_serving::{
encode_done_sse, encode_json_sse as encode_json_sse_impl, map_claude_stop_reason,
PipelineFinalizeError,
AiSurfaceFinalizeError,
};
use crate::GatewayError;
fn map_error(err: PipelineFinalizeError) -> GatewayError {
fn map_error(err: AiSurfaceFinalizeError) -> GatewayError {
err.into()
}

View File

@@ -1,9 +1,6 @@
//! Standard finalize surface for standard contract sync/stream compilation.
#[path = "stream_core/mod.rs"]
mod stream;
pub(crate) use crate::ai_pipeline::{
pub(crate) use crate::ai_serving::{
aggregate_standard_chat_stream_sync_response, aggregate_standard_cli_stream_sync_response,
build_openai_responses_response, convert_claude_chat_response_to_openai_chat,
convert_claude_response_to_openai_responses, convert_gemini_chat_response_to_openai_chat,
@@ -20,4 +17,3 @@ pub(crate) use crate::ai_pipeline::{
maybe_build_standard_sync_finalize_product_from_normalized_payload,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
};
pub(crate) use stream::*;

View File

@@ -1,6 +1,6 @@
use serde_json::json;
use crate::ai_pipeline::maybe_bridge_standard_sync_json_to_stream;
use crate::ai_serving::maybe_bridge_standard_sync_json_to_stream;
use super::maybe_build_local_stream_rewriter;

View File

@@ -11,8 +11,8 @@ use super::{
convert_gemini_chat_response_to_openai_chat, convert_gemini_response_to_openai_responses,
maybe_build_local_core_sync_finalize_response,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::{
use crate::ai_serving::GatewayControlDecision;
use crate::ai_serving::{
convert_openai_chat_response_to_openai_responses,
convert_openai_responses_response_to_openai_chat,
};

View File

@@ -1,6 +1,5 @@
mod adaptation;
mod contracts;
mod conversion;
pub(crate) mod api;
mod finalize;
mod planner;
mod pure;
@@ -8,12 +7,9 @@ pub(crate) mod transport;
use axum::body::Body;
use axum::http::{Response, Uri};
use serde_json::{json, Value};
use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
use self::contracts::ExecutionRuntimeAuthContext;
pub(crate) use self::adaptation::{
maybe_build_provider_private_stream_normalizer, ProviderPrivateStreamNormalizer,
};
@@ -47,9 +43,23 @@ pub(crate) use self::planner::{
LocalResolvedOAuthRequestAuth, PlannerAppState,
};
pub(crate) use self::pure::*;
pub(crate) use self::transport::{
append_transport_diagnostics_to_value, build_request_trace_proxy_value,
candidate_common_transport_skip_reason, candidate_transport_pair_skip_reason,
request_conversion_direct_auth, request_conversion_enabled_for_transport,
request_conversion_transport_supported, request_conversion_transport_unsupported_reason,
request_pair_allowed_for_transport, CandidateTransportPolicyFacts,
};
pub(crate) use crate::control::GatewayControlDecision;
pub(crate) use crate::execution_runtime::{ConversionMode, ExecutionStrategy};
pub(crate) use crate::headers::RequestOrigin;
pub(crate) use aether_ai_serving::{
ai_local_execution_contract_for_formats, augment_sync_report_context,
build_ai_report_context_original_request_echo as build_report_context_original_request_echo,
extract_ai_gemini_model_from_path as extract_gemini_model_from_path,
generic_decision_missing_exact_provider_request as generic_decision_missing_exact_provider_request_impl,
AiExecutionDecision, AiExecutionPlanPayload, AiStreamAttempt, AiSyncAttempt,
};
pub(crate) fn build_provider_transport_request_url(
transport: &GatewayProviderTransportSnapshot,
@@ -59,9 +69,9 @@ pub(crate) fn build_provider_transport_request_url(
request_query: Option<&str>,
kiro_api_region: Option<&str>,
) -> Option<String> {
crate::provider_transport::build_transport_request_url(
self::transport::build_transport_request_url(
transport,
crate::provider_transport::TransportRequestUrlParams {
self::transport::TransportRequestUrlParams {
provider_api_format,
mapped_model,
upstream_is_stream,
@@ -96,38 +106,10 @@ pub(crate) fn request_origin_from_parts(parts: &http::request::Parts) -> Request
crate::headers::request_origin_from_parts(parts)
}
pub(crate) fn build_report_context_original_request_echo(
body_json: Option<&Value>,
body_bytes_b64: Option<&str>,
) -> Option<Value> {
if let Some(body_bytes_b64) = body_bytes_b64
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(json!({ "body_bytes_b64": body_bytes_b64 }));
}
body_json.filter(|body| !body.is_null()).cloned()
}
pub(crate) fn is_json_request(headers: &http::HeaderMap) -> bool {
crate::headers::is_json_request(headers)
}
pub(crate) fn extract_gemini_model_from_path(path: &str) -> Option<String> {
let (_, suffix) = path.split_once("/models/")?;
let model = suffix
.split_once(':')
.map(|(value, _)| value)
.unwrap_or(suffix);
let model = model.trim();
if model.is_empty() {
None
} else {
Some(model.to_string())
}
}
pub(crate) fn build_execution_runtime_auth_context(
auth_context: &crate::control::GatewayControlAuthContext,
) -> ExecutionRuntimeAuthContext {
@@ -159,6 +141,22 @@ pub(crate) fn resolve_local_decision_execution_runtime_auth_context(
})
}
pub(crate) fn generic_decision_missing_exact_provider_request(
payload: &AiExecutionDecision,
) -> bool {
if !generic_decision_missing_exact_provider_request_impl(payload) {
return false;
}
tracing::warn!(
decision_kind = payload.decision_kind.as_deref().unwrap_or_default(),
provider_api_format = payload.provider_api_format.as_deref().unwrap_or_default(),
client_api_format = payload.client_api_format.as_deref().unwrap_or_default(),
"gateway generic decision missing exact provider request; falling back to plan"
);
true
}
pub(crate) fn maybe_build_local_sync_finalize_response(
trace_id: &str,
decision: &GatewayControlDecision,
@@ -166,45 +164,3 @@ pub(crate) fn maybe_build_local_sync_finalize_response(
) -> Result<Option<Response<Body>>, GatewayError> {
crate::execution_runtime::maybe_build_local_sync_finalize_response(trace_id, decision, payload)
}
#[cfg(test)]
mod tests {
use super::{build_report_context_original_request_echo, extract_gemini_model_from_path};
use serde_json::json;
#[test]
fn build_report_context_original_request_echo_preserves_full_request_body() {
let body = json!({
"messages": [{"role": "user", "content": "large payload should be omitted"}],
"service_tier": "default",
"instructions": "Be concise.",
"thinking": {"type": "enabled", "budget_tokens": 512},
"metadata": {"trace": "keep"},
"body_bytes_b64": "aGVsbG8=",
});
let echo = build_report_context_original_request_echo(Some(&body), None)
.expect("echo should be produced");
assert_eq!(echo, body);
}
#[test]
fn build_report_context_original_request_echo_prefers_binary_body_bytes() {
let echo = build_report_context_original_request_echo(
Some(&json!({"ignored": true})),
Some("aGVsbG8="),
)
.expect("echo should be produced");
assert_eq!(echo, json!({"body_bytes_b64": "aGVsbG8="}));
}
#[test]
fn extract_gemini_model_from_path_trims_method_suffix() {
let model =
extract_gemini_model_from_path("/v1beta/models/gemini-2.5-pro:streamGenerateContent");
assert_eq!(model.as_deref(), Some("gemini-2.5-pro"));
}
}

View File

@@ -3,7 +3,7 @@ use aether_scheduler_core::{
SchedulerMinimalCandidateSelectionCandidate,
};
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL;
const PLANNER_SCHEDULER_AFFINITY_MAX_ENTRIES: usize = 10_000;

View File

@@ -1,16 +1,27 @@
use aether_ai_serving::{
ai_candidate_extra_data_with_ranking, ai_should_persist_available_candidate_for_pool_key,
ai_should_persist_skipped_candidate_for_pool_membership,
run_ai_available_candidate_persistence, run_ai_candidate_materialization,
run_ai_skipped_candidate_persistence, AiAvailableCandidatePersistencePort,
AiCandidateMaterializationOutcome, AiCandidateMaterializationPort, AiCandidateResolutionMode,
AiSkippedCandidatePersistencePort,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use aether_scheduler_core::SchedulerRankingOutcome;
use async_trait::async_trait;
use serde_json::Value;
use std::convert::Infallible;
use uuid::Uuid;
use crate::ai_pipeline::planner::candidate_affinity_cache::remember_scheduler_affinity_for_candidate;
use crate::ai_pipeline::planner::candidate_metadata::append_ranking_metadata_to_object;
use crate::ai_pipeline::planner::candidate_resolution::{
use crate::ai_serving::planner::candidate_affinity_cache::remember_scheduler_affinity_for_candidate;
use crate::ai_serving::planner::candidate_resolution::{
resolve_and_rank_local_execution_candidates,
resolve_and_rank_local_execution_candidates_without_transport_pair_gate,
EligibleLocalExecutionCandidate, SkippedLocalExecutionCandidate,
};
use crate::ai_pipeline::planner::failure_diagnostic::CandidateFailureDiagnostic;
use crate::ai_pipeline::planner::runtime_miss::record_local_runtime_candidate_skip_reason;
use crate::ai_pipeline::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::ai_serving::planner::materialization_policy::LocalCandidatePersistencePolicy;
use crate::ai_serving::planner::runtime_miss::record_local_runtime_candidate_skip_reason;
use crate::ai_serving::planner::CandidateFailureDiagnostic;
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_ms;
use crate::handlers::shared::provider_pool::admin_provider_pool_config_from_config_value;
use crate::orchestration::{local_attempt_slot_count, ExecutionAttemptIdentity};
@@ -48,6 +59,292 @@ pub(crate) struct LocalSkippedCandidatePersistenceContext<'a> {
pub(crate) record_runtime_miss_diagnostic: bool,
}
pub(crate) use aether_ai_serving::AiCandidateResolutionMode as LocalCandidateResolutionMode;
struct GatewayLocalCandidateMaterializationPort<'a, F, G> {
state: PlannerAppState<'a>,
trace_id: &'a str,
client_api_format: &'a str,
requested_model: Option<&'a str>,
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&'a Value>,
sticky_session_token: Option<&'a str>,
persistence_policy: LocalCandidatePersistencePolicy<'a>,
resolution_mode: LocalCandidateResolutionMode,
build_available_extra_data: F,
decorate_skipped_candidate: G,
}
struct GatewayAvailableCandidatePersistencePort<'a, F> {
state: PlannerAppState<'a>,
trace_id: &'a str,
user_id: &'a str,
api_key_id: &'a str,
required_capabilities: Option<&'a Value>,
error_context: &'static str,
created_at_unix_ms: u64,
build_extra_data: F,
}
struct GatewaySkippedCandidatePersistencePort<'a> {
state: &'a AppState,
trace_id: &'a str,
user_id: &'a str,
api_key_id: &'a str,
required_capabilities: Option<&'a Value>,
error_context: &'static str,
record_runtime_miss_diagnostic: bool,
}
#[async_trait]
impl<F, G> AiCandidateMaterializationPort for GatewayLocalCandidateMaterializationPort<'_, F, G>
where
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
G: Fn(SkippedLocalExecutionCandidate) -> SkippedLocalExecutionCandidate + Send + Sync,
{
type Candidate = SchedulerMinimalCandidateSelectionCandidate;
type Eligible = EligibleLocalExecutionCandidate;
type Skipped = SkippedLocalExecutionCandidate;
type Attempt = LocalExecutionCandidateAttempt;
type Error = Infallible;
async fn resolve_and_rank_candidates(
&self,
candidates: Vec<Self::Candidate>,
) -> Result<(Vec<Self::Eligible>, Vec<Self::Skipped>), Self::Error> {
let requested_model = self.requested_model.map(str::to_string);
let resolved = match self.resolution_mode {
AiCandidateResolutionMode::Standard => {
resolve_and_rank_local_execution_candidates(
self.state,
candidates,
self.client_api_format,
requested_model.as_deref().unwrap_or_default(),
self.auth_snapshot,
self.required_capabilities,
self.sticky_session_token,
)
.await
}
AiCandidateResolutionMode::WithoutTransportPairGate => {
resolve_and_rank_local_execution_candidates_without_transport_pair_gate(
self.state,
candidates,
self.client_api_format,
requested_model.as_deref(),
self.auth_snapshot,
self.required_capabilities,
self.sticky_session_token,
)
.await
}
};
Ok(resolved)
}
fn decorate_skipped_candidate(&self, skipped: Self::Skipped) -> Self::Skipped {
(self.decorate_skipped_candidate)(skipped)
}
fn remember_first_candidate_affinity(&self, candidates: &[Self::Eligible]) {
remember_first_local_candidate_affinity(
self.state,
self.auth_snapshot,
self.client_api_format,
self.requested_model,
candidates,
);
}
async fn persist_available_candidates(
&self,
candidates: Vec<Self::Eligible>,
) -> Result<Vec<Self::Attempt>, Self::Error> {
Ok(persist_available_local_execution_candidates_with_context(
self.state,
self.trace_id,
self.persistence_policy.available,
candidates,
&self.build_available_extra_data,
)
.await)
}
async fn persist_skipped_candidates(
&self,
starting_candidate_index: u32,
skipped_candidates: Vec<Self::Skipped>,
) -> Result<(), Self::Error> {
persist_skipped_local_execution_candidates_with_context(
self.state.app(),
self.trace_id,
self.persistence_policy.skipped,
starting_candidate_index,
skipped_candidates,
)
.await;
Ok(())
}
}
#[async_trait]
impl<F> AiAvailableCandidatePersistencePort for GatewayAvailableCandidatePersistencePort<'_, F>
where
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
{
type Candidate = EligibleLocalExecutionCandidate;
type Attempt = LocalExecutionCandidateAttempt;
type ExtraData = Value;
type Error = Infallible;
fn attempt_slot_count(&self, candidate: &Self::Candidate) -> u32 {
local_attempt_slot_count(&candidate.transport)
}
fn build_extra_data(&self, candidate: &Self::Candidate) -> Option<Self::ExtraData> {
ai_candidate_extra_data_with_ranking(
(self.build_extra_data)(candidate),
candidate.ranking.as_ref(),
)
}
fn generate_candidate_id(&self) -> String {
Uuid::new_v4().to_string()
}
fn should_persist_available_candidate(&self, candidate: &Self::Candidate) -> bool {
should_persist_available_local_candidate(candidate)
}
async fn persist_available_candidate(
&self,
candidate: &Self::Candidate,
candidate_index: u32,
retry_index: u32,
generated_candidate_id: &str,
extra_data: Option<Self::ExtraData>,
) -> Result<String, Self::Error> {
Ok(self
.state
.persist_available_local_candidate(
self.trace_id,
self.user_id,
self.api_key_id,
&candidate.candidate,
candidate_index,
retry_index,
generated_candidate_id,
self.required_capabilities,
extra_data,
self.created_at_unix_ms,
self.error_context,
)
.await)
}
fn build_attempt(
&self,
candidate: Self::Candidate,
candidate_index: u32,
retry_index: u32,
candidate_id: String,
) -> Self::Attempt {
LocalExecutionCandidateAttempt {
eligible: candidate,
candidate_index,
retry_index,
candidate_id,
}
}
}
#[async_trait]
impl AiSkippedCandidatePersistencePort for GatewaySkippedCandidatePersistencePort<'_> {
type Skipped = SkippedLocalExecutionCandidate;
type ExtraData = Value;
type Error = Infallible;
fn should_persist_skipped_candidate(&self, candidate: &Self::Skipped) -> bool {
should_persist_skipped_local_candidate(candidate)
}
fn build_extra_data(&self, candidate: &Self::Skipped) -> Option<Self::ExtraData> {
ai_candidate_extra_data_with_ranking(
candidate.extra_data.clone(),
candidate.ranking.as_ref(),
)
}
fn generate_candidate_id(&self) -> String {
Uuid::new_v4().to_string()
}
async fn persist_skipped_candidate(
&self,
candidate: &Self::Skipped,
candidate_index: u32,
generated_candidate_id: &str,
extra_data: Option<Self::ExtraData>,
) -> Result<(), Self::Error> {
persist_skipped_local_execution_candidate(
self.state,
self.trace_id,
self.user_id,
self.api_key_id,
&candidate.candidate,
candidate_index,
generated_candidate_id,
self.required_capabilities,
candidate.skip_reason,
extra_data,
self.error_context,
self.record_runtime_miss_diagnostic,
)
.await;
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn materialize_local_execution_candidates_with_serving<F, G>(
state: PlannerAppState<'_>,
trace_id: &str,
client_api_format: &str,
requested_model: Option<&str>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&Value>,
sticky_session_token: Option<&str>,
persistence_policy: LocalCandidatePersistencePolicy<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
preselection_skipped: Vec<SkippedLocalExecutionCandidate>,
resolution_mode: LocalCandidateResolutionMode,
build_available_extra_data: F,
decorate_skipped_candidate: G,
) -> AiCandidateMaterializationOutcome<LocalExecutionCandidateAttempt>
where
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
G: Fn(SkippedLocalExecutionCandidate) -> SkippedLocalExecutionCandidate + Send + Sync,
{
let port = GatewayLocalCandidateMaterializationPort {
state,
trace_id,
client_api_format,
requested_model,
auth_snapshot,
required_capabilities,
sticky_session_token,
persistence_policy,
resolution_mode,
build_available_extra_data,
decorate_skipped_candidate,
};
match run_ai_candidate_materialization(&port, candidates, preselection_skipped).await {
Ok(outcome) => outcome,
Err(error) => match error {},
}
}
pub(crate) fn remember_first_local_candidate_affinity(
state: PlannerAppState<'_>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
@@ -72,16 +369,14 @@ pub(crate) fn remember_first_local_candidate_affinity(
}
fn should_persist_available_local_candidate(eligible: &EligibleLocalExecutionCandidate) -> bool {
eligible
.orchestration
.pool_key_index
.is_none_or(|index| index == 0)
ai_should_persist_available_candidate_for_pool_key(eligible.orchestration.pool_key_index)
}
fn should_persist_skipped_local_candidate(candidate: &SkippedLocalExecutionCandidate) -> bool {
candidate.transport.as_ref().is_none_or(|transport| {
admin_provider_pool_config_from_config_value(transport.provider.config.as_ref()).is_none()
})
let is_pool_candidate = candidate.transport.as_ref().is_some_and(|transport| {
admin_provider_pool_config_from_config_value(transport.provider.config.as_ref()).is_some()
});
ai_should_persist_skipped_candidate_for_pool_membership(is_pool_candidate)
}
#[allow(clippy::too_many_arguments)]
@@ -96,90 +391,23 @@ pub(crate) async fn persist_available_local_execution_candidates<F>(
build_extra_data: F,
) -> Vec<LocalExecutionCandidateAttempt>
where
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value>,
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
{
let created_at_unix_ms = current_unix_ms();
let total_attempts = candidates
.iter()
.map(|eligible| local_attempt_slot_count(&eligible.transport) as usize)
.sum();
let mut materialized = Vec::with_capacity(total_attempts);
let port = GatewayAvailableCandidatePersistencePort {
state,
trace_id,
user_id,
api_key_id,
required_capabilities,
error_context,
created_at_unix_ms: current_unix_ms(),
build_extra_data,
};
for (candidate_index, eligible) in candidates.into_iter().enumerate() {
let candidate_index = candidate_index as u32;
let attempt_slots = local_attempt_slot_count(&eligible.transport);
let pool_key_index = eligible.orchestration.pool_key_index;
let extra_data = local_candidate_extra_data_with_ranking(
build_extra_data(&eligible),
eligible.ranking.as_ref(),
);
let mut owned_eligible = Some(eligible);
for retry_index in 0..attempt_slots {
let eligible = owned_eligible
.as_ref()
.expect("eligible candidate should remain available until final retry");
let attempt_identity = ExecutionAttemptIdentity::new(candidate_index, retry_index)
.with_pool_key_index(pool_key_index);
let generated_candidate_id = Uuid::new_v4().to_string();
let candidate_id = if should_persist_available_local_candidate(eligible) {
state
.persist_available_local_candidate(
trace_id,
user_id,
api_key_id,
&eligible.candidate,
attempt_identity.candidate_index,
attempt_identity.retry_index,
&generated_candidate_id,
required_capabilities,
extra_data.clone(),
created_at_unix_ms,
error_context,
)
.await
} else {
generated_candidate_id
};
let eligible = if retry_index + 1 == attempt_slots {
owned_eligible
.take()
.expect("final retry should consume owned eligible candidate")
} else {
eligible.clone()
};
materialized.push(LocalExecutionCandidateAttempt {
eligible,
candidate_index: attempt_identity.candidate_index,
retry_index: attempt_identity.retry_index,
candidate_id,
});
}
match run_ai_available_candidate_persistence(&port, candidates).await {
Ok(attempts) => attempts,
Err(error) => match error {},
}
materialized
}
fn local_candidate_extra_data_with_ranking(
extra_data: Option<Value>,
ranking: Option<&SchedulerRankingOutcome>,
) -> Option<Value> {
let Some(ranking) = ranking else {
return extra_data;
};
let mut object = match extra_data {
Some(Value::Object(object)) => object,
Some(value) => {
let mut object = serde_json::Map::new();
object.insert("extra".to_string(), value);
object
}
None => serde_json::Map::new(),
};
append_ranking_metadata_to_object(&mut object, ranking);
Some(Value::Object(object))
}
pub(crate) async fn persist_available_local_execution_candidates_with_context<F>(
@@ -190,7 +418,7 @@ pub(crate) async fn persist_available_local_execution_candidates_with_context<F>
build_extra_data: F,
) -> Vec<LocalExecutionCandidateAttempt>
where
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value>,
F: Fn(&EligibleLocalExecutionCandidate) -> Option<Value> + Send + Sync,
{
persist_available_local_execution_candidates(
state,
@@ -330,31 +558,21 @@ pub(crate) async fn persist_skipped_local_execution_candidates(
error_context: &'static str,
record_runtime_miss_diagnostic: bool,
) {
let mut next_candidate_index = starting_candidate_index;
for skipped_candidate in skipped_candidates {
if !should_persist_skipped_local_candidate(&skipped_candidate) {
continue;
}
let generated_candidate_id = Uuid::new_v4().to_string();
persist_skipped_local_execution_candidate(
state,
trace_id,
user_id,
api_key_id,
&skipped_candidate.candidate,
next_candidate_index,
&generated_candidate_id,
required_capabilities,
skipped_candidate.skip_reason,
local_candidate_extra_data_with_ranking(
skipped_candidate.extra_data,
skipped_candidate.ranking.as_ref(),
),
error_context,
record_runtime_miss_diagnostic,
)
.await;
next_candidate_index = next_candidate_index.saturating_add(1);
let port = GatewaySkippedCandidatePersistencePort {
state,
trace_id,
user_id,
api_key_id,
required_capabilities,
error_context,
record_runtime_miss_diagnostic,
};
match run_ai_skipped_candidate_persistence(&port, starting_candidate_index, skipped_candidates)
.await
{
Ok(()) => {}
Err(error) => match error {},
}
}
@@ -423,8 +641,8 @@ mod tests {
fn sample_transport(
key_id: &str,
provider_config: Option<serde_json::Value>,
) -> Arc<crate::ai_pipeline::GatewayProviderTransportSnapshot> {
Arc::new(crate::ai_pipeline::GatewayProviderTransportSnapshot {
) -> Arc<crate::ai_serving::GatewayProviderTransportSnapshot> {
Arc::new(crate::ai_serving::GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "provider-1".to_string(),

View File

@@ -0,0 +1,320 @@
use aether_ai_serving::{
append_ai_execution_contract_fields_to_value, append_ai_ranking_metadata_to_object,
build_ai_candidate_metadata_from_candidate,
};
use aether_scheduler_core::{SchedulerMinimalCandidateSelectionCandidate, SchedulerRankingOutcome};
use serde_json::{Map, Value};
use crate::ai_serving::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_serving::transport::append_transport_diagnostics_to_value;
use crate::ai_serving::GatewayProviderTransportSnapshot;
use crate::ai_serving::{ConversionMode, ExecutionStrategy};
pub(crate) struct LocalExecutionCandidateMetadataParts<'a> {
pub(crate) eligible: &'a EligibleLocalExecutionCandidate,
pub(crate) provider_api_format: &'a str,
pub(crate) client_api_format: &'a str,
pub(crate) extra_fields: Map<String, Value>,
}
pub(crate) fn append_ranking_metadata_to_object(
object: &mut Map<String, Value>,
ranking: &SchedulerRankingOutcome,
) {
append_ai_ranking_metadata_to_object(object, ranking);
}
pub(crate) fn build_local_execution_candidate_metadata(
parts: LocalExecutionCandidateMetadataParts<'_>,
) -> Value {
build_local_execution_candidate_metadata_for_candidate(
&parts.eligible.candidate,
Some(parts.eligible.transport.as_ref()),
parts.provider_api_format,
parts.client_api_format,
parts.extra_fields,
)
}
pub(crate) fn build_local_execution_candidate_metadata_for_candidate(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: Option<&GatewayProviderTransportSnapshot>,
provider_api_format: &str,
client_api_format: &str,
extra_fields: Map<String, Value>,
) -> Value {
append_transport_diagnostics_to_value(
build_ai_candidate_metadata_from_candidate(
candidate,
provider_api_format,
client_api_format,
extra_fields,
),
transport,
client_api_format,
provider_api_format,
)
}
pub(crate) fn build_local_execution_candidate_contract_metadata(
parts: LocalExecutionCandidateMetadataParts<'_>,
execution_strategy: ExecutionStrategy,
conversion_mode: ConversionMode,
provider_contract: &str,
) -> Value {
append_ai_execution_contract_fields_to_value(
build_local_execution_candidate_metadata_for_candidate(
&parts.eligible.candidate,
Some(parts.eligible.transport.as_ref()),
parts.provider_api_format,
parts.client_api_format,
parts.extra_fields,
),
execution_strategy.as_str(),
conversion_mode.as_str(),
parts.client_api_format,
provider_contract,
)
}
pub(crate) fn build_local_execution_candidate_contract_metadata_for_candidate(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
transport: Option<&GatewayProviderTransportSnapshot>,
provider_api_format: &str,
client_api_format: &str,
extra_fields: Map<String, Value>,
execution_strategy: ExecutionStrategy,
conversion_mode: ConversionMode,
provider_contract: &str,
) -> Value {
append_ai_execution_contract_fields_to_value(
build_local_execution_candidate_metadata_for_candidate(
candidate,
transport,
provider_api_format,
client_api_format,
extra_fields,
),
execution_strategy.as_str(),
conversion_mode.as_str(),
client_api_format,
provider_contract,
)
}
#[cfg(test)]
mod tests {
use super::{
build_local_execution_candidate_contract_metadata_for_candidate,
build_local_execution_candidate_metadata_for_candidate,
};
use crate::ai_serving::transport::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
use crate::ai_serving::{ConversionMode, ExecutionStrategy, GatewayProviderTransportSnapshot};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use serde_json::{json, Value};
fn sample_candidate() -> SchedulerMinimalCandidateSelectionCandidate {
SchedulerMinimalCandidateSelectionCandidate {
provider_id: "provider-1".to_string(),
provider_name: "RightCode".to_string(),
provider_type: "codex".to_string(),
provider_priority: 22,
endpoint_id: "endpoint-1".to_string(),
endpoint_api_format: "openai:responses".to_string(),
key_id: "key-1".to_string(),
key_name: "codex".to_string(),
key_auth_type: "oauth".to_string(),
key_internal_priority: 10,
key_global_priority_for_format: None,
key_capabilities: None,
model_id: "model-1".to_string(),
global_model_id: "global-1".to_string(),
global_model_name: "gpt-5.4".to_string(),
selected_provider_model_name: "gpt-5.4".to_string(),
mapping_matched_model: None,
}
}
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "RightCode".to_string(),
provider_type: "codex".to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: Some(json!({"enabled": true, "mode": "node", "node_id": "proxy-node-1"})),
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "openai:responses".to_string(),
api_family: None,
endpoint_kind: None,
is_active: true,
base_url: "https://example.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: Some("/v1/responses".to_string()),
config: None,
format_acceptance_config: Some(json!({
"enabled": true,
"accept_formats": ["claude:messages"]
})),
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "codex".to_string(),
auth_type: "oauth".to_string(),
is_active: true,
api_formats: None,
auth_type_by_format: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: Some(json!({
"tls_profile": "chrome_136",
"user_agent": "Mozilla/5.0"
})),
decrypted_api_key: "sk-test".to_string(),
decrypted_auth_config: None,
},
}
}
fn sample_claude_code_transport_without_auth() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-cc-1".to_string(),
name: "NekoCode".to_string(),
provider_type: "claude_code".to_string(),
website: Some("https://nekocode.ai".to_string()),
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
base_url: "https://api.anthropic.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
name: "CC-特价-0.4".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["claude:messages".to_string()]),
auth_type_by_format: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
decrypted_api_key: "__placeholder__".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn candidate_contract_metadata_includes_transport_diagnostics() {
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
&sample_candidate(),
Some(&sample_transport()),
"openai:responses",
"claude:messages",
serde_json::Map::new(),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
"openai:responses",
);
assert_eq!(metadata["transport_diagnostics"]["provider_type"], "codex");
assert_eq!(
metadata["transport_diagnostics"]["fingerprint"]["tls_profile"],
"chrome_136"
);
assert_eq!(
metadata["transport_diagnostics"]["resolved_tls_profile"],
"chrome_136"
);
assert_eq!(
metadata["transport_diagnostics"]["request_pair"]["conversion_enabled"],
Value::Bool(true)
);
assert!(
metadata["transport_diagnostics"]["request_pair"]["transport_unsupported_reason"]
.is_null()
);
}
#[test]
fn candidate_metadata_marks_missing_transport_snapshot() {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
None,
"openai:responses",
"openai:responses",
serde_json::Map::new(),
);
assert_eq!(
metadata["transport_diagnostics"]["transport_snapshot_available"],
Value::Bool(false)
);
}
#[test]
fn candidate_metadata_uses_same_format_provider_specific_transport_reason() {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
Some(&sample_claude_code_transport_without_auth()),
"claude:messages",
"claude:messages",
serde_json::Map::new(),
);
assert_eq!(
metadata["transport_diagnostics"]["request_pair"]["transport_unsupported_reason"],
Value::String("transport_auth_unavailable".to_string())
);
}
}

View File

@@ -1,16 +1,15 @@
use aether_ai_serving::{
prepare_ai_header_authenticated_candidate, resolve_ai_candidate_mapped_model,
AiPreparedHeaderAuthenticatedCandidate,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use tracing::warn;
use crate::ai_pipeline::{
use crate::ai_serving::{
GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth, PlannerAppState,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PreparedHeaderAuthenticatedCandidate {
pub(crate) auth_header: String,
pub(crate) auth_value: String,
pub(crate) mapped_model: String,
}
pub(crate) type PreparedHeaderAuthenticatedCandidate = AiPreparedHeaderAuthenticatedCandidate;
#[derive(Debug, Clone, Copy)]
pub(crate) struct OauthPreparationContext<'a> {
@@ -36,27 +35,29 @@ pub(crate) async fn prepare_header_authenticated_candidate(
None
};
let Some((auth_header, auth_value)) = direct_auth.or(oauth_auth) else {
return Err("transport_auth_unavailable");
};
let mapped_model = resolve_candidate_mapped_model(candidate)?;
prepare_ai_header_authenticated_candidate(
direct_auth,
oauth_auth,
candidate.selected_provider_model_name.as_str(),
)
}
Ok(PreparedHeaderAuthenticatedCandidate {
auth_header,
auth_value,
mapped_model,
})
pub(crate) fn prepare_header_authenticated_candidate_from_auth(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
auth_header: String,
auth_value: String,
) -> Result<PreparedHeaderAuthenticatedCandidate, &'static str> {
prepare_ai_header_authenticated_candidate(
Some((auth_header, auth_value)),
None,
candidate.selected_provider_model_name.as_str(),
)
}
pub(crate) fn resolve_candidate_mapped_model(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> Result<String, &'static str> {
let mapped_model = candidate.selected_provider_model_name.trim().to_string();
if mapped_model.is_empty() {
return Err("mapped_model_missing");
}
Ok(mapped_model)
resolve_ai_candidate_mapped_model(candidate.selected_provider_model_name.as_str())
}
pub(crate) async fn resolve_candidate_oauth_auth(
@@ -92,7 +93,7 @@ mod tests {
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use super::{prepare_header_authenticated_candidate, OauthPreparationContext};
use crate::ai_pipeline::PlannerAppState;
use crate::ai_serving::PlannerAppState;
fn sample_transport() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {

View File

@@ -1,19 +1,21 @@
use std::collections::BTreeMap;
use aether_ai_serving::{
ai_ranking_context, build_ai_rankable_candidate, run_ai_candidate_ranking,
AiCandidateRankingPort, AiRankableCandidateParts, AiRankingContextConfig,
AiRankingSchedulingMode,
};
use async_trait::async_trait;
use tracing::warn;
use crate::ai_pipeline::{
request_candidate_api_format_preference, GatewayAuthApiKeySnapshot, PlannerAppState,
};
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::handlers::shared::provider_pool::admin_provider_pool_config_from_config_value;
use crate::scheduler::config::{
read_scheduler_ordering_config, SchedulerOrderingConfig, SchedulerSchedulingMode,
};
use aether_scheduler_core::{
apply_scheduler_candidate_ranking, matches_affinity_target,
requested_capability_priority_for_candidate, SchedulerAffinityTarget,
SchedulerMinimalCandidateSelectionCandidate, SchedulerRankableCandidate,
SchedulerRankingContext, SchedulerRankingMode,
matches_affinity_target, SchedulerAffinityTarget, SchedulerMinimalCandidateSelectionCandidate,
SchedulerRankableCandidate, SchedulerRankingContext, SchedulerRankingOutcome,
};
use super::candidate_affinity_cache::read_cached_scheduler_affinity_target;
@@ -22,6 +24,92 @@ use super::candidate_transport_ranking_facts::{
resolve_cached_transport_ranking_facts, CandidateTransportRankingFacts,
};
struct GatewayLocalCandidateRankingPort<'a> {
state: PlannerAppState<'a>,
requested_model: Option<&'a str>,
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&'a serde_json::Value>,
ordering_config: SchedulerOrderingConfig,
}
#[async_trait]
impl AiCandidateRankingPort for GatewayLocalCandidateRankingPort<'_> {
type Candidate = EligibleLocalExecutionCandidate;
type AffinityTarget = SchedulerAffinityTarget;
type Error = std::convert::Infallible;
fn affinity_requested_model(&self, candidates: &[Self::Candidate]) -> Option<String> {
self.requested_model
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| {
candidates
.first()
.map(|candidate| candidate.candidate.global_model_name.clone())
})
}
async fn read_cached_affinity_target(
&self,
normalized_client_api_format: &str,
affinity_requested_model: Option<&str>,
) -> Result<Option<Self::AffinityTarget>, Self::Error> {
Ok(read_cached_scheduler_affinity_target(
self.state,
self.auth_snapshot,
normalized_client_api_format,
affinity_requested_model,
))
}
fn cached_affinity_matches(
&self,
candidate: &Self::Candidate,
target: &Self::AffinityTarget,
) -> bool {
cached_affinity_matches_local_execution_scope(candidate, target)
}
async fn build_rankable_candidate(
&self,
candidate: &Self::Candidate,
original_index: usize,
normalized_client_api_format: &str,
cached_affinity_match: bool,
) -> Result<SchedulerRankableCandidate, Self::Error> {
let ranking_facts = resolve_transport_ranking_facts_for_candidate(
self.state,
&candidate.candidate,
candidate.transport.as_ref(),
self.ordering_config,
)
.await;
Ok(build_ai_rankable_candidate(AiRankableCandidateParts {
candidate: &candidate.candidate,
original_index,
normalized_client_api_format,
provider_api_format: candidate.provider_api_format.as_str(),
required_capabilities: self.required_capabilities,
cached_affinity_match,
tunnel_bucket: ranking_facts.tunnel_bucket,
keep_priority_on_conversion: ranking_facts.keep_priority_on_conversion,
}))
}
fn ranking_context(&self) -> SchedulerRankingContext {
ai_ranking_context(ai_ranking_context_config(self.ordering_config))
}
fn apply_ranking_outcome(
&self,
candidate: &mut Self::Candidate,
outcome: SchedulerRankingOutcome,
) {
candidate.ranking = Some(outcome);
}
}
pub(crate) async fn rank_eligible_local_execution_candidates(
state: PlannerAppState<'_>,
candidates: Vec<EligibleLocalExecutionCandidate>,
@@ -31,89 +119,35 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
required_capabilities: Option<&serde_json::Value>,
) -> Vec<EligibleLocalExecutionCandidate> {
let ordering_config = read_scheduler_ordering_config_or_default(state).await;
let mut candidates = candidates;
let affinity_requested_model = requested_model
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
candidates
.first()
.map(|candidate| candidate.candidate.global_model_name.as_str())
});
let cached_affinity_target = read_cached_scheduler_affinity_target(
let port = GatewayLocalCandidateRankingPort {
state,
requested_model,
auth_snapshot,
normalized_client_api_format,
affinity_requested_model,
);
let mut rankables = Vec::with_capacity(candidates.len());
let mut ordering_cache = BTreeMap::new();
required_capabilities,
ordering_config,
};
for (original_index, eligible) in candidates.iter().enumerate() {
let ranking_facts = resolve_cached_transport_ranking_facts(
state,
&mut ordering_cache,
&eligible.candidate,
eligible.transport.as_ref(),
ordering_config,
)
.await;
rankables.push(rankable_candidate_from_candidate(
&eligible.candidate,
original_index,
ranking_facts,
normalized_client_api_format,
eligible.provider_api_format.as_str(),
required_capabilities,
cached_affinity_target.as_ref().is_some_and(|target| {
cached_affinity_matches_local_execution_scope(eligible, target)
}),
));
match run_ai_candidate_ranking(&port, candidates, normalized_client_api_format).await {
Ok(candidates) => candidates,
Err(error) => match error {},
}
drop(ordering_cache);
let outcomes = apply_scheduler_candidate_ranking(
&mut candidates,
&rankables,
planner_ranking_context(ordering_config),
);
for outcome in outcomes {
let ranking_index = outcome.ranking_index;
if let Some(candidate) = candidates.get_mut(ranking_index) {
candidate.ranking = Some(outcome);
}
}
candidates
}
fn rankable_candidate_from_candidate(
async fn resolve_transport_ranking_facts_for_candidate(
state: PlannerAppState<'_>,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
original_index: usize,
ranking_facts: CandidateTransportRankingFacts,
normalized_client_api_format: &str,
provider_api_format: &str,
required_capabilities: Option<&serde_json::Value>,
cached_affinity_match: bool,
) -> SchedulerRankableCandidate {
let is_same_format = api_format_matches(provider_api_format, normalized_client_api_format);
let mut rankable = SchedulerRankableCandidate::from_candidate(candidate, original_index);
// The scheduler order is the upstream tie-breaker; pipeline only adds transport facts.
rankable.provider_id.clear();
rankable.endpoint_id.clear();
rankable.key_id.clear();
rankable.selected_provider_model_name.clear();
rankable
.with_capability_priority(requested_capability_priority_for_candidate(
required_capabilities,
candidate,
))
.with_cached_affinity_match(cached_affinity_match)
.with_tunnel_bucket(ranking_facts.tunnel_bucket)
.with_format_state(
!is_same_format && !ranking_facts.keep_priority_on_conversion,
candidate_api_format_preference(normalized_client_api_format, provider_api_format),
)
transport: &crate::ai_serving::GatewayProviderTransportSnapshot,
ordering_config: SchedulerOrderingConfig,
) -> CandidateTransportRankingFacts {
let mut ordering_cache = BTreeMap::new();
resolve_cached_transport_ranking_facts(
state,
&mut ordering_cache,
candidate,
transport,
ordering_config,
)
.await
}
fn cached_affinity_matches_local_execution_scope(
@@ -133,36 +167,21 @@ fn local_execution_candidate_uses_pool(eligible: &EligibleLocalExecutionCandidat
.is_some()
}
fn planner_ranking_context(ordering_config: SchedulerOrderingConfig) -> SchedulerRankingContext {
SchedulerRankingContext {
fn ai_ranking_context_config(ordering_config: SchedulerOrderingConfig) -> AiRankingContextConfig {
AiRankingContextConfig {
priority_mode: ordering_config.priority_mode,
ranking_mode: planner_ranking_mode(ordering_config.scheduling_mode),
include_health: false,
load_balance_seed: 0,
scheduling_mode: ai_ranking_scheduling_mode(ordering_config.scheduling_mode),
}
}
fn planner_ranking_mode(mode: SchedulerSchedulingMode) -> SchedulerRankingMode {
fn ai_ranking_scheduling_mode(mode: SchedulerSchedulingMode) -> AiRankingSchedulingMode {
match mode {
SchedulerSchedulingMode::FixedOrder => SchedulerRankingMode::FixedOrder,
SchedulerSchedulingMode::CacheAffinity => SchedulerRankingMode::CacheAffinity,
SchedulerSchedulingMode::LoadBalance => SchedulerRankingMode::LoadBalance,
SchedulerSchedulingMode::FixedOrder => AiRankingSchedulingMode::FixedOrder,
SchedulerSchedulingMode::CacheAffinity => AiRankingSchedulingMode::CacheAffinity,
SchedulerSchedulingMode::LoadBalance => AiRankingSchedulingMode::LoadBalance,
}
}
fn normalize_api_format_alias(value: &str) -> String {
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn candidate_api_format_preference(client_api_format: &str, provider_api_format: &str) -> (u8, u8) {
request_candidate_api_format_preference(client_api_format, provider_api_format)
.unwrap_or((u8::MAX, u8::MAX))
}
async fn read_scheduler_ordering_config_or_default(
state: PlannerAppState<'_>,
) -> SchedulerOrderingConfig {
@@ -185,6 +204,9 @@ mod tests {
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use aether_ai_serving::{
ai_ranking_context, build_ai_rankable_candidate, AiRankableCandidateParts,
};
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
@@ -197,7 +219,7 @@ mod tests {
use super::super::candidate_affinity_cache::remember_scheduler_affinity_for_candidate;
use super::super::candidate_transport_ranking_facts::resolve_cached_candidate_transport_ranking_facts;
use super::{PlannerAppState, SchedulerMinimalCandidateSelectionCandidate};
use crate::ai_pipeline::planner::candidate_resolution::resolve_and_rank_local_execution_candidates;
use crate::ai_serving::planner::candidate_resolution::resolve_and_rank_local_execution_candidates;
use crate::data::auth::GatewayAuthApiKeySnapshot;
use crate::data::GatewayDataState;
use crate::tunnel::TunnelAttachmentRecord;
@@ -224,22 +246,23 @@ mod tests {
ordering_config,
)
.await;
rankables.push(super::rankable_candidate_from_candidate(
rankables.push(build_ai_rankable_candidate(AiRankableCandidateParts {
candidate,
original_index,
ranking_facts,
normalized_client_api_format.as_str(),
candidate.endpoint_api_format.as_str(),
normalized_client_api_format: normalized_client_api_format.as_str(),
provider_api_format: candidate.endpoint_api_format.as_str(),
required_capabilities,
false,
));
cached_affinity_match: false,
tunnel_bucket: ranking_facts.tunnel_bucket,
keep_priority_on_conversion: ranking_facts.keep_priority_on_conversion,
}));
}
drop(ordering_cache);
apply_scheduler_candidate_ranking(
&mut candidates,
&rankables,
super::planner_ranking_context(ordering_config),
ai_ranking_context(super::ai_ranking_context_config(ordering_config)),
);
candidates
}

View File

@@ -0,0 +1,287 @@
use std::sync::Arc;
use aether_ai_serving::{
run_ai_candidate_resolution, AiCandidateResolutionMode, AiCandidateResolutionPort,
AiCandidateResolutionRequest,
};
use async_trait::async_trait;
use std::convert::Infallible;
use tracing::warn;
use aether_scheduler_core::{SchedulerMinimalCandidateSelectionCandidate, SchedulerRankingOutcome};
use crate::ai_serving::{
candidate_common_transport_skip_reason, candidate_transport_pair_skip_reason,
CandidateTransportPolicyFacts, GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot,
PlannerAppState,
};
use crate::orchestration::LocalExecutionCandidateMetadata;
use super::candidate_ranking::rank_eligible_local_execution_candidates;
use super::pool_scheduler::apply_local_execution_pool_scheduler;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EligibleLocalExecutionCandidate {
pub(crate) candidate: SchedulerMinimalCandidateSelectionCandidate,
pub(crate) transport: Arc<GatewayProviderTransportSnapshot>,
pub(crate) provider_api_format: String,
pub(crate) orchestration: LocalExecutionCandidateMetadata,
pub(crate) ranking: Option<SchedulerRankingOutcome>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SkippedLocalExecutionCandidate {
pub(crate) candidate: SchedulerMinimalCandidateSelectionCandidate,
pub(crate) skip_reason: &'static str,
pub(crate) transport: Option<Arc<GatewayProviderTransportSnapshot>>,
pub(crate) ranking: Option<SchedulerRankingOutcome>,
pub(crate) extra_data: Option<serde_json::Value>,
}
impl SkippedLocalExecutionCandidate {
pub(crate) fn transport_ref(&self) -> Option<&GatewayProviderTransportSnapshot> {
self.transport.as_deref()
}
}
struct GatewayLocalCandidateResolutionPort<'a> {
state: PlannerAppState<'a>,
requested_model: Option<&'a str>,
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&'a serde_json::Value>,
sticky_session_token: Option<&'a str>,
}
#[async_trait]
impl AiCandidateResolutionPort for GatewayLocalCandidateResolutionPort<'_> {
type Candidate = SchedulerMinimalCandidateSelectionCandidate;
type Transport = GatewayProviderTransportSnapshot;
type Eligible = EligibleLocalExecutionCandidate;
type Skipped = SkippedLocalExecutionCandidate;
type Error = Infallible;
async fn read_candidate_transport(
&self,
candidate: &Self::Candidate,
) -> Result<Option<Self::Transport>, Self::Error> {
Ok(read_candidate_transport_snapshot(self.state, candidate).await)
}
fn build_missing_transport_skipped_candidate(
&self,
candidate: Self::Candidate,
) -> Self::Skipped {
SkippedLocalExecutionCandidate {
candidate,
skip_reason: "transport_snapshot_missing",
transport: None,
ranking: None,
extra_data: None,
}
}
fn candidate_common_skip_reason(
&self,
candidate: &Self::Candidate,
transport: &Self::Transport,
requested_model: Option<&str>,
) -> Option<&'static str> {
candidate_common_transport_skip_reason(
transport,
candidate_transport_policy_facts(candidate),
requested_model,
)
}
fn candidate_transport_pair_skip_reason(
&self,
candidate: &Self::Candidate,
transport: &Self::Transport,
normalized_client_api_format: &str,
requested_model: &str,
) -> Option<&'static str> {
let _ = (candidate, requested_model);
candidate_transport_pair_skip_reason(transport, normalized_client_api_format)
}
fn build_skipped_candidate(
&self,
candidate: Self::Candidate,
transport: Self::Transport,
skip_reason: &'static str,
) -> Self::Skipped {
SkippedLocalExecutionCandidate {
candidate,
skip_reason,
transport: Some(Arc::new(transport)),
ranking: None,
extra_data: None,
}
}
fn build_eligible_candidate(
&self,
candidate: Self::Candidate,
transport: Self::Transport,
) -> Self::Eligible {
let provider_api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
EligibleLocalExecutionCandidate {
candidate,
transport: Arc::new(transport),
provider_api_format,
orchestration: LocalExecutionCandidateMetadata::default(),
ranking: None,
}
}
async fn rank_eligible_candidates(
&self,
candidates: Vec<Self::Eligible>,
normalized_client_api_format: &str,
) -> Result<Vec<Self::Eligible>, Self::Error> {
Ok(rank_eligible_local_execution_candidates(
self.state,
candidates,
normalized_client_api_format,
self.requested_model,
self.auth_snapshot,
self.required_capabilities,
)
.await)
}
async fn apply_pool_scheduler(
&self,
candidates: Vec<Self::Eligible>,
) -> Result<(Vec<Self::Eligible>, Vec<Self::Skipped>), Self::Error> {
Ok(
apply_local_execution_pool_scheduler(self.state, candidates, self.sticky_session_token)
.await,
)
}
}
pub(crate) async fn resolve_and_rank_local_execution_candidates(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: &str,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let requested_model = requested_model.trim();
resolve_and_rank_local_execution_candidates_with_mode(
state,
candidates,
client_api_format,
Some(requested_model),
auth_snapshot,
required_capabilities,
sticky_session_token,
AiCandidateResolutionMode::Standard,
)
.await
}
pub(crate) async fn resolve_and_rank_local_execution_candidates_without_transport_pair_gate(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: Option<&str>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let requested_model = requested_model.map(str::trim);
resolve_and_rank_local_execution_candidates_with_mode(
state,
candidates,
client_api_format,
requested_model,
auth_snapshot,
required_capabilities,
sticky_session_token,
AiCandidateResolutionMode::WithoutTransportPairGate,
)
.await
}
async fn resolve_and_rank_local_execution_candidates_with_mode(
state: PlannerAppState<'_>,
candidates: Vec<SchedulerMinimalCandidateSelectionCandidate>,
client_api_format: &str,
requested_model: Option<&str>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
sticky_session_token: Option<&str>,
mode: AiCandidateResolutionMode,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let port = GatewayLocalCandidateResolutionPort {
state,
requested_model,
auth_snapshot,
required_capabilities,
sticky_session_token,
};
let request = AiCandidateResolutionRequest {
client_api_format,
requested_model,
mode,
};
match run_ai_candidate_resolution(&port, candidates, request).await {
Ok(outcome) => (outcome.eligible_candidates, outcome.skipped_candidates),
Err(error) => match error {},
}
}
fn candidate_transport_policy_facts(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> CandidateTransportPolicyFacts<'_> {
CandidateTransportPolicyFacts {
endpoint_api_format: candidate.endpoint_api_format.as_str(),
global_model_name: candidate.global_model_name.as_str(),
selected_provider_model_name: candidate.selected_provider_model_name.as_str(),
mapping_matched_model: candidate.mapping_matched_model.as_deref(),
}
}
pub(crate) async fn read_candidate_transport_snapshot(
state: PlannerAppState<'_>,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> Option<GatewayProviderTransportSnapshot> {
match state
.read_provider_transport_snapshot(
&candidate.provider_id,
&candidate.endpoint_id,
&candidate.key_id,
)
.await
{
Ok(Some(transport)) => Some(transport),
Ok(None) => None,
Err(error) => {
warn!(
event_name = "candidate_resolution_transport_load_failed",
log_type = "event",
provider_id = %candidate.provider_id,
endpoint_id = %candidate.endpoint_id,
key_id = %candidate.key_id,
error = ?error,
"failed to load provider transport while evaluating local candidate eligibility"
);
None
}
}
}

View File

@@ -0,0 +1,218 @@
use aether_ai_serving::{
run_ai_candidate_preselection, AiCandidatePreselectionOutcome, AiCandidatePreselectionPort,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use async_trait::async_trait;
use crate::ai_serving::planner::candidate_resolution::SkippedLocalExecutionCandidate;
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::scheduler::candidate::SchedulerSkippedCandidate;
use crate::GatewayError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LocalCandidatePreselectionKeyMode {
ProviderEndpointKeyModel,
ProviderEndpointKeyModelAndApiFormat,
}
struct GatewayLocalCandidatePreselectionPort<'a> {
state: PlannerAppState<'a>,
client_api_format: &'a str,
requested_model: &'a str,
require_streaming: bool,
required_capabilities: Option<&'a serde_json::Value>,
auth_snapshot: &'a GatewayAuthApiKeySnapshot,
use_api_format_alias_match: bool,
key_mode: LocalCandidatePreselectionKeyMode,
}
#[async_trait]
impl AiCandidatePreselectionPort for GatewayLocalCandidatePreselectionPort<'_> {
type Candidate = SchedulerMinimalCandidateSelectionCandidate;
type Skipped = SkippedLocalExecutionCandidate;
type Error = GatewayError;
fn candidate_api_formats(&self) -> Vec<String> {
crate::ai_serving::request_candidate_api_formats(
self.client_api_format,
self.require_streaming,
)
.into_iter()
.map(str::to_string)
.collect()
}
fn candidate_api_format_matches_client(&self, candidate_api_format: &str) -> bool {
if self.use_api_format_alias_match {
crate::ai_serving::api_format_alias_matches(
candidate_api_format,
self.client_api_format,
)
} else {
candidate_api_format == self.client_api_format
}
}
async fn list_candidates_for_api_format(
&self,
candidate_api_format: &str,
matches_client_format: bool,
) -> Result<(Vec<Self::Candidate>, Vec<Self::Skipped>), Self::Error> {
let auth_snapshot = matches_client_format.then_some(self.auth_snapshot);
let (candidates, skipped_candidates) = self
.state
.list_selectable_candidates_with_skip_reasons(
candidate_api_format,
self.requested_model,
self.require_streaming,
self.required_capabilities,
auth_snapshot,
current_unix_secs(),
)
.await?;
Ok((
candidates,
skipped_candidates
.into_iter()
.map(skipped_local_execution_candidate_from_scheduler_skip)
.collect(),
))
}
fn candidate_allowed(
&self,
candidate: &Self::Candidate,
_candidate_api_format: &str,
matches_client_format: bool,
) -> bool {
matches_client_format
|| auth_snapshot_allows_cross_format_candidate(
self.auth_snapshot,
self.requested_model,
candidate,
)
}
fn skipped_candidate_allowed(
&self,
skipped_candidate: &Self::Skipped,
_candidate_api_format: &str,
matches_client_format: bool,
) -> bool {
matches_client_format
|| auth_snapshot_allows_cross_format_candidate(
self.auth_snapshot,
self.requested_model,
&skipped_candidate.candidate,
)
}
fn candidate_key(&self, candidate: &Self::Candidate) -> String {
local_candidate_preselection_key(candidate, self.key_mode)
}
fn skipped_candidate_key(&self, skipped_candidate: &Self::Skipped) -> String {
local_candidate_preselection_key(&skipped_candidate.candidate, self.key_mode)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn preselect_local_execution_candidates_with_serving(
state: PlannerAppState<'_>,
client_api_format: &str,
requested_model: &str,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: &GatewayAuthApiKeySnapshot,
use_api_format_alias_match: bool,
key_mode: LocalCandidatePreselectionKeyMode,
) -> Result<
AiCandidatePreselectionOutcome<
SchedulerMinimalCandidateSelectionCandidate,
SkippedLocalExecutionCandidate,
>,
GatewayError,
> {
let port = GatewayLocalCandidatePreselectionPort {
state,
client_api_format,
requested_model,
require_streaming,
required_capabilities,
auth_snapshot,
use_api_format_alias_match,
key_mode,
};
run_ai_candidate_preselection(&port).await
}
fn skipped_local_execution_candidate_from_scheduler_skip(
skipped_candidate: SchedulerSkippedCandidate,
) -> SkippedLocalExecutionCandidate {
SkippedLocalExecutionCandidate {
candidate: skipped_candidate.candidate,
skip_reason: skipped_candidate.skip_reason,
transport: None,
ranking: None,
extra_data: None,
}
}
fn local_candidate_preselection_key(
candidate: &SchedulerMinimalCandidateSelectionCandidate,
mode: LocalCandidatePreselectionKeyMode,
) -> String {
match mode {
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModel => format!(
"{}:{}:{}:{}:{}",
candidate.provider_id,
candidate.endpoint_id,
candidate.key_id,
candidate.model_id,
candidate.selected_provider_model_name,
),
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat => format!(
"{}:{}:{}:{}:{}:{}",
candidate.provider_id,
candidate.endpoint_id,
candidate.key_id,
candidate.model_id,
candidate.selected_provider_model_name,
candidate.endpoint_api_format,
),
}
}
pub(crate) fn auth_snapshot_allows_cross_format_candidate(
auth_snapshot: &GatewayAuthApiKeySnapshot,
requested_model: &str,
candidate: &SchedulerMinimalCandidateSelectionCandidate,
) -> bool {
if let Some(allowed_providers) = auth_snapshot.effective_allowed_providers() {
let provider_allowed = allowed_providers.iter().any(|value| {
aether_scheduler_core::provider_matches_allowed_value(
value,
&candidate.provider_id,
&candidate.provider_name,
&candidate.provider_type,
)
});
if !provider_allowed {
return false;
}
}
if let Some(allowed_models) = auth_snapshot.effective_allowed_models() {
let model_allowed = allowed_models
.iter()
.any(|value| value == requested_model || value == &candidate.global_model_name);
if !model_allowed {
return false;
}
}
true
}

View File

@@ -5,7 +5,7 @@ use aether_scheduler_core::{
};
use tracing::warn;
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, PlannerAppState};
use crate::ai_serving::{GatewayProviderTransportSnapshot, PlannerAppState};
use crate::scheduler::config::SchedulerOrderingConfig;
use super::candidate_resolution::read_candidate_transport_snapshot;

View File

@@ -0,0 +1,131 @@
use axum::body::Bytes;
use crate::ai_serving::{
force_upstream_streaming_for_provider as force_upstream_streaming_for_provider_impl,
is_json_request, parse_direct_request_body as parse_direct_request_body_impl,
};
pub(crate) use crate::ai_serving::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
pub(crate) use aether_ai_serving::AiRequestedModelFamily as RequestedModelFamily;
pub(crate) fn parse_direct_request_body(
parts: &http::request::Parts,
body_bytes: &Bytes,
) -> Option<(serde_json::Value, Option<String>)> {
parse_direct_request_body_impl(is_json_request(&parts.headers), body_bytes.as_ref())
}
pub(crate) fn force_upstream_streaming_for_provider(
provider_type: &str,
provider_api_format: &str,
) -> bool {
force_upstream_streaming_for_provider_impl(provider_type, provider_api_format)
}
pub(crate) fn extract_standard_requested_model(body_json: &serde_json::Value) -> Option<String> {
aether_ai_serving::extract_ai_standard_requested_model(body_json)
}
pub(crate) fn extract_requested_model_from_request(
parts: &http::request::Parts,
body_json: &serde_json::Value,
family: RequestedModelFamily,
) -> Option<String> {
aether_ai_serving::extract_ai_requested_model_from_request_path(
parts.uri.path(),
body_json,
family,
)
}
#[cfg(test)]
mod tests {
use super::{
extract_requested_model_from_request, extract_standard_requested_model,
force_upstream_streaming_for_provider, RequestedModelFamily,
};
use axum::http::Request;
use serde_json::json;
#[test]
fn forces_streaming_for_codex_openai_responses() {
assert!(force_upstream_streaming_for_provider(
"codex",
"openai:responses"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[test]
fn does_not_force_streaming_for_compact_or_other_provider_types() {
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"openai",
"openai:responses"
));
}
#[test]
fn extracts_standard_requested_model_from_request_body() {
let requested_model =
extract_standard_requested_model(&json!({ "model": " claude-sonnet-4 " }));
assert_eq!(requested_model.as_deref(), Some("claude-sonnet-4"));
}
#[test]
fn request_family_helper_delegates_standard_model_extraction() {
let request = Request::builder()
.uri("https://example.test/v1/chat/completions")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let requested_model = extract_requested_model_from_request(
&parts,
&json!({ "model": " claude-sonnet-4 " }),
RequestedModelFamily::Standard,
);
assert_eq!(requested_model.as_deref(), Some("claude-sonnet-4"));
}
#[test]
fn extracts_gemini_requested_model_from_request_path() {
let request = Request::builder()
.uri("https://example.test/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let requested_model =
extract_requested_model_from_request(&parts, &json!({}), RequestedModelFamily::Gemini);
assert_eq!(requested_model.as_deref(), Some("gemini-2.5-pro"));
}
}

View File

@@ -1,33 +1,32 @@
use crate::ai_pipeline::contracts::ExecutionRuntimeAuthContext;
use crate::ai_pipeline::planner::common::{
use crate::ai_serving::planner::common::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, EXECUTION_RUNTIME_STREAM_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND,
GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
use crate::ai_pipeline::planner::plan_builders::{
use crate::ai_serving::planner::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_openai_chat_stream_plan_from_decision, build_openai_chat_sync_plan_from_decision,
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_stream_plan_from_decision,
build_passthrough_sync_plan_from_decision, build_standard_stream_plan_from_decision,
build_standard_sync_plan_from_decision, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
build_standard_sync_plan_from_decision,
};
use crate::ai_pipeline::planner::route::{
use crate::ai_serving::planner::route::{
resolve_execution_runtime_stream_plan_kind as resolve_stream_plan_kind,
resolve_execution_runtime_sync_plan_kind as resolve_sync_plan_kind,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::{
AppState, GatewayControlPlanResponse, GatewayControlSyncDecisionResponse, GatewayError,
use crate::ai_serving::GatewayControlDecision;
use crate::{AiExecutionDecision, AiExecutionPlanPayload, AppState, GatewayError};
use aether_ai_serving::{
build_ai_stream_execution_plan_payload, build_ai_sync_execution_plan_payload,
};
pub(crate) async fn maybe_build_sync_plan_payload_impl(
@@ -38,7 +37,7 @@ pub(crate) async fn maybe_build_sync_plan_payload_impl(
body_json: &serde_json::Value,
body_base64: Option<&str>,
body_is_empty: bool,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
let Some(plan_kind) = resolve_sync_plan_kind(parts, decision) else {
return Ok(None);
};
@@ -66,7 +65,7 @@ pub(crate) async fn maybe_build_stream_plan_payload_impl(
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
let Some(plan_kind) = resolve_stream_plan_kind(parts, decision) else {
return Ok(None);
};
@@ -90,8 +89,8 @@ fn build_sync_plan_payload_from_decision(
parts: &http::request::Parts,
body_json: &serde_json::Value,
plan_kind: &str,
mut payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
mut payload: AiExecutionDecision,
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
let auth_context = payload.auth_context.take();
let plan_and_report = match plan_kind {
OPENAI_CHAT_SYNC_PLAN_KIND => {
@@ -124,15 +123,16 @@ fn build_sync_plan_payload_from_decision(
_ => None,
};
Ok(plan_and_report.map(|value| build_sync_plan_response(plan_kind, value, auth_context)))
Ok(plan_and_report
.map(|value| build_ai_sync_execution_plan_payload(plan_kind, value, auth_context)))
}
fn build_stream_plan_payload_from_decision(
parts: &http::request::Parts,
body_json: &serde_json::Value,
plan_kind: &str,
mut payload: GatewayControlSyncDecisionResponse,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
mut payload: AiExecutionDecision,
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
let auth_context = payload.auth_context.take();
let plan_and_report = match plan_kind {
OPENAI_CHAT_STREAM_PLAN_KIND => {
@@ -159,35 +159,6 @@ fn build_stream_plan_payload_from_decision(
_ => None,
};
Ok(plan_and_report.map(|value| build_stream_plan_response(plan_kind, value, auth_context)))
}
fn build_sync_plan_response(
plan_kind: &str,
value: LocalSyncPlanAndReport,
auth_context: Option<ExecutionRuntimeAuthContext>,
) -> GatewayControlPlanResponse {
GatewayControlPlanResponse {
action: EXECUTION_RUNTIME_SYNC_ACTION.to_string(),
plan_kind: Some(plan_kind.to_string()),
plan: Some(value.plan),
report_kind: value.report_kind,
report_context: value.report_context,
auth_context,
}
}
fn build_stream_plan_response(
plan_kind: &str,
value: LocalStreamPlanAndReport,
auth_context: Option<ExecutionRuntimeAuthContext>,
) -> GatewayControlPlanResponse {
GatewayControlPlanResponse {
action: EXECUTION_RUNTIME_STREAM_ACTION.to_string(),
plan_kind: Some(plan_kind.to_string()),
plan: Some(value.plan),
report_kind: value.report_kind,
report_context: value.report_context,
auth_context,
}
Ok(plan_and_report
.map(|value| build_ai_stream_execution_plan_payload(plan_kind, value, auth_context)))
}

View File

@@ -0,0 +1,188 @@
use aether_ai_serving::{
build_ai_execution_decision_from_plan, run_ai_stream_decision_path,
AiExecutionDecisionFromPlanParts, AiStreamDecisionPathPort, AiStreamDecisionStep,
};
use async_trait::async_trait;
use crate::ai_serving::planner::common::{
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, OPENAI_VIDEO_CONTENT_PLAN_KIND,
};
use crate::ai_serving::planner::route::{
is_matching_stream_request, resolve_execution_runtime_stream_plan_kind,
};
use crate::ai_serving::{resolve_decision_execution_runtime_auth_context, GatewayControlDecision};
use crate::{AiExecutionDecision, AppState, GatewayError};
pub(crate) async fn maybe_build_stream_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(plan_kind) = resolve_execution_runtime_stream_plan_kind(parts, decision) else {
return Ok(None);
};
if !is_matching_stream_request(plan_kind, parts, body_json, body_base64) {
return Ok(None);
}
let port = GatewayStreamDecisionPathPort {
state,
parts,
trace_id,
decision,
body_json,
body_base64,
plan_kind,
};
run_ai_stream_decision_path(&port).await
}
struct GatewayStreamDecisionPathPort<'a> {
state: &'a AppState,
parts: &'a http::request::Parts,
trace_id: &'a str,
decision: &'a GatewayControlDecision,
body_json: &'a serde_json::Value,
body_base64: Option<&'a str>,
plan_kind: &'a str,
}
#[async_trait]
impl AiStreamDecisionPathPort for GatewayStreamDecisionPathPort<'_> {
type Decision = AiExecutionDecision;
type Error = GatewayError;
async fn build_stream_decision_step(
&self,
step: AiStreamDecisionStep,
) -> Result<Option<Self::Decision>, Self::Error> {
match step {
AiStreamDecisionStep::LocalVideoContent => {
maybe_build_local_video_task_content_stream_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalImage => {
super::maybe_build_stream_local_image_decision_payload(
self.state,
self.parts,
self.body_json,
self.body_base64,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalOpenAiChat => {
super::maybe_build_stream_local_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalOpenAiResponses => {
super::maybe_build_stream_local_openai_responses_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalStandardFamily => {
super::maybe_build_stream_local_standard_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalSameFormatProvider => {
super::maybe_build_stream_local_same_format_provider_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiStreamDecisionStep::LocalGeminiFiles => {
super::maybe_build_stream_local_gemini_files_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
}
}
}
async fn maybe_build_local_video_task_content_stream_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<AiExecutionDecision>, GatewayError> {
if plan_kind != OPENAI_VIDEO_CONTENT_PLAN_KIND
|| decision.route_family.as_deref() != Some("openai")
{
return Ok(None);
}
let _ = state
.hydrate_video_task_for_route(decision.route_family.as_deref(), parts.uri.path())
.await?;
let Some(action) = state.video_tasks.prepare_openai_content_stream_action(
parts.uri.path(),
parts.uri.query(),
trace_id,
) else {
return Ok(None);
};
let crate::video_tasks::LocalVideoTaskContentAction::StreamPlan(plan) = action else {
return Ok(None);
};
let plan = *plan;
Ok(Some(build_ai_execution_decision_from_plan(
AiExecutionDecisionFromPlanParts {
action: EXECUTION_RUNTIME_STREAM_DECISION_ACTION.to_string(),
decision_kind: Some(plan_kind.to_string()),
request_id: None,
upstream_base_url: None,
include_auth_pair: false,
plan,
report_kind: None,
report_context: None,
auth_context: resolve_decision_execution_runtime_auth_context(decision),
},
)))
}

View File

@@ -0,0 +1,257 @@
use aether_ai_serving::{
build_ai_execution_decision_from_plan, infer_ai_upstream_base_url, run_ai_sync_decision_path,
AiExecutionDecisionFromPlanParts, AiSyncDecisionPathPort, AiSyncDecisionStep,
};
use async_trait::async_trait;
use tracing::debug;
use crate::ai_serving::planner::common::{
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
use crate::ai_serving::planner::route::resolve_execution_runtime_sync_plan_kind;
use crate::ai_serving::{
build_execution_runtime_auth_context, resolve_execution_runtime_auth_context,
GatewayControlDecision,
};
use crate::{AiExecutionDecision, AppState, GatewayError};
pub(crate) async fn maybe_build_sync_decision_payload(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
body_is_empty: bool,
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(plan_kind) = resolve_execution_runtime_sync_plan_kind(parts, decision) else {
return Ok(None);
};
let port = GatewaySyncDecisionPathPort {
state,
parts,
trace_id,
decision,
body_json,
body_base64,
body_is_empty,
plan_kind,
};
run_ai_sync_decision_path(&port).await
}
struct GatewaySyncDecisionPathPort<'a> {
state: &'a AppState,
parts: &'a http::request::Parts,
trace_id: &'a str,
decision: &'a GatewayControlDecision,
body_json: &'a serde_json::Value,
body_base64: Option<&'a str>,
body_is_empty: bool,
plan_kind: &'a str,
}
#[async_trait]
impl AiSyncDecisionPathPort for GatewaySyncDecisionPathPort<'_> {
type Decision = AiExecutionDecision;
type Error = GatewayError;
fn sync_decision_step_enabled(&self, step: AiSyncDecisionStep) -> bool {
if step == AiSyncDecisionStep::LocalGeminiFiles {
return matches!(
self.plan_kind,
GEMINI_FILES_LIST_PLAN_KIND
| GEMINI_FILES_GET_PLAN_KIND
| GEMINI_FILES_DELETE_PLAN_KIND
);
}
true
}
async fn build_sync_decision_step(
&self,
step: AiSyncDecisionStep,
) -> Result<Option<Self::Decision>, Self::Error> {
match step {
AiSyncDecisionStep::VideoTaskFollowUp => {
maybe_build_local_video_task_follow_up_sync_decision_payload(
self.state,
self.parts,
self.body_json,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalVideo => {
super::maybe_build_sync_local_video_decision_payload(
self.state,
self.parts,
self.body_json,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalImage => {
super::maybe_build_sync_local_image_decision_payload(
self.state,
self.parts,
self.body_json,
self.body_base64,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalOpenAiChat => {
super::maybe_build_sync_local_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalOpenAiResponses => {
super::maybe_build_sync_local_openai_responses_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalStandardFamily => {
super::maybe_build_sync_local_standard_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalSameFormatProvider => {
super::maybe_build_sync_local_same_format_provider_decision_payload(
self.state,
self.parts,
self.trace_id,
self.decision,
self.body_json,
self.plan_kind,
)
.await
}
AiSyncDecisionStep::LocalGeminiFiles => {
super::maybe_build_sync_local_gemini_files_decision_payload(
self.state,
self.parts,
self.body_json,
self.body_base64,
self.body_is_empty,
self.trace_id,
self.decision,
self.plan_kind,
)
.await
}
}
}
}
async fn maybe_build_local_video_task_follow_up_sync_decision_payload(
state: &AppState,
parts: &http::request::Parts,
body_json: &serde_json::Value,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<AiExecutionDecision>, GatewayError> {
if !matches!(
plan_kind,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
| OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND
| OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND
| GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND
) {
return Ok(None);
}
let _ = state
.hydrate_video_task_for_route(decision.route_family.as_deref(), parts.uri.path())
.await?;
let auth_context = resolve_execution_runtime_auth_context(
state,
decision,
&parts.headers,
&parts.uri,
trace_id,
)
.await?;
let Some(auth_context) = auth_context else {
return Ok(None);
};
let Some(follow_up) = state.video_tasks.prepare_follow_up_sync_plan(
plan_kind,
parts.uri.path(),
Some(body_json),
Some(&auth_context),
trace_id,
) else {
return Ok(None);
};
let aether_video_tasks_core::LocalVideoTaskFollowUpPlan {
plan,
report_kind,
report_context,
} = follow_up;
let upstream_base_url = infer_ai_upstream_base_url(&plan.url);
debug!(
event_name = "local_video_follow_up_sync_decision_payload_built",
log_type = "debug",
trace_id = %trace_id,
request_id = %trace_id,
candidate_id = ?plan.candidate_id,
provider_id = %plan.provider_id,
endpoint_id = %plan.endpoint_id,
key_id = %plan.key_id,
plan_kind,
downstream_path = %parts.uri.path(),
provider_api_format = %plan.provider_api_format,
client_api_format = %plan.client_api_format,
upstream_base_url = ?upstream_base_url,
upstream_url = %plan.url,
"gateway built local video follow-up sync decision payload"
);
Ok(Some(build_ai_execution_decision_from_plan(
AiExecutionDecisionFromPlanParts {
action: EXECUTION_RUNTIME_SYNC_DECISION_ACTION.to_string(),
decision_kind: Some(plan_kind.to_string()),
request_id: Some(trace_id.to_string()),
upstream_base_url,
include_auth_pair: true,
plan,
report_kind,
report_context,
auth_context: Some(build_execution_runtime_auth_context(&auth_context)),
},
)))
}

View File

@@ -0,0 +1,127 @@
use aether_ai_serving::{run_ai_authenticated_decision_input, AiAuthenticatedDecisionInputPort};
use async_trait::async_trait;
use crate::ai_serving::{ExecutionRuntimeAuthContext, GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::clock::current_unix_secs;
use crate::{AppState, GatewayError};
#[derive(Debug, Clone)]
pub(crate) struct ResolvedLocalDecisionAuthInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub(crate) struct LocalRequestedModelDecisionInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) requested_model: String,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub(crate) struct LocalAuthenticatedDecisionInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
pub(crate) auth_snapshot: GatewayAuthApiKeySnapshot,
pub(crate) required_capabilities: Option<serde_json::Value>,
}
struct GatewayAuthenticatedDecisionInputPort<'a> {
state: PlannerAppState<'a>,
now_unix_secs: u64,
}
#[async_trait]
impl AiAuthenticatedDecisionInputPort for GatewayAuthenticatedDecisionInputPort<'_> {
type AuthContext = ExecutionRuntimeAuthContext;
type AuthSnapshot = GatewayAuthApiKeySnapshot;
type RequiredCapabilities = serde_json::Value;
type ResolvedInput = ResolvedLocalDecisionAuthInput;
type Error = GatewayError;
async fn read_auth_snapshot(
&self,
auth_context: &Self::AuthContext,
) -> Result<Option<Self::AuthSnapshot>, Self::Error> {
self.state
.read_auth_api_key_snapshot(
&auth_context.user_id,
&auth_context.api_key_id,
self.now_unix_secs,
)
.await
}
async fn resolve_required_capabilities(
&self,
auth_context: &Self::AuthContext,
requested_model: Option<&str>,
explicit_required_capabilities: Option<&Self::RequiredCapabilities>,
) -> Result<Option<Self::RequiredCapabilities>, Self::Error> {
Ok(self
.state
.resolve_request_candidate_required_capabilities(
&auth_context.user_id,
&auth_context.api_key_id,
requested_model,
explicit_required_capabilities,
)
.await)
}
fn build_resolved_input(
&self,
auth_context: Self::AuthContext,
auth_snapshot: Self::AuthSnapshot,
required_capabilities: Option<Self::RequiredCapabilities>,
) -> Self::ResolvedInput {
ResolvedLocalDecisionAuthInput {
auth_context,
auth_snapshot,
required_capabilities,
}
}
}
pub(crate) fn build_local_requested_model_decision_input(
resolved_input: ResolvedLocalDecisionAuthInput,
requested_model: String,
) -> LocalRequestedModelDecisionInput {
LocalRequestedModelDecisionInput {
auth_context: resolved_input.auth_context,
requested_model,
auth_snapshot: resolved_input.auth_snapshot,
required_capabilities: resolved_input.required_capabilities,
}
}
pub(crate) fn build_local_authenticated_decision_input(
resolved_input: ResolvedLocalDecisionAuthInput,
) -> LocalAuthenticatedDecisionInput {
LocalAuthenticatedDecisionInput {
auth_context: resolved_input.auth_context,
auth_snapshot: resolved_input.auth_snapshot,
required_capabilities: resolved_input.required_capabilities,
}
}
pub(crate) async fn resolve_local_authenticated_decision_input(
state: &AppState,
auth_context: ExecutionRuntimeAuthContext,
requested_model: Option<&str>,
explicit_required_capabilities: Option<&serde_json::Value>,
) -> Result<Option<ResolvedLocalDecisionAuthInput>, GatewayError> {
let port = GatewayAuthenticatedDecisionInputPort {
state: PlannerAppState::new(state),
now_unix_secs: current_unix_secs(),
};
run_ai_authenticated_decision_input(
&port,
auth_context,
requested_model,
explicit_required_capabilities,
)
.await
}

View File

@@ -0,0 +1,38 @@
use aether_ai_serving::ai_candidate_persistence_policy_spec;
pub(crate) use aether_ai_serving::AiCandidatePersistencePolicyKind as LocalCandidatePersistencePolicyKind;
use serde_json::Value;
use crate::ai_serving::planner::candidate_materialization::{
LocalAvailableCandidatePersistenceContext, LocalSkippedCandidatePersistenceContext,
};
use crate::ai_serving::ExecutionRuntimeAuthContext;
#[derive(Debug, Clone, Copy)]
pub(crate) struct LocalCandidatePersistencePolicy<'a> {
pub(crate) available: LocalAvailableCandidatePersistenceContext<'a>,
pub(crate) skipped: LocalSkippedCandidatePersistenceContext<'a>,
}
pub(crate) fn build_local_candidate_persistence_policy<'a>(
auth_context: &'a ExecutionRuntimeAuthContext,
required_capabilities: Option<&'a Value>,
kind: LocalCandidatePersistencePolicyKind,
) -> LocalCandidatePersistencePolicy<'a> {
let spec = ai_candidate_persistence_policy_spec(kind);
LocalCandidatePersistencePolicy {
available: LocalAvailableCandidatePersistenceContext {
user_id: &auth_context.user_id,
api_key_id: &auth_context.api_key_id,
required_capabilities,
error_context: spec.available_error_context,
},
skipped: LocalSkippedCandidatePersistenceContext {
user_id: &auth_context.user_id,
api_key_id: &auth_context.api_key_id,
required_capabilities,
error_context: spec.skipped_error_context,
record_runtime_miss_diagnostic: spec.record_runtime_miss_diagnostic,
},
}
}

View File

@@ -1,7 +1,4 @@
use crate::ai_pipeline::contracts::{
GatewayControlPlanResponse, GatewayControlSyncDecisionResponse,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_serving::{AiExecutionDecision, AiExecutionPlanPayload, GatewayControlDecision};
use crate::{AppState, GatewayError};
mod candidate_affinity_cache;
@@ -15,10 +12,8 @@ mod candidate_transport_ranking_facts;
mod common;
mod decision;
mod decision_input;
mod failure_diagnostic;
mod materialization_policy;
mod passthrough;
mod payload_metadata;
mod plan_builders;
mod pool_scheduler;
mod report_context;
@@ -29,10 +24,6 @@ mod specialized;
mod standard;
mod state;
pub(crate) use self::candidate_resolution::extract_pool_sticky_session_token;
pub(crate) use self::failure_diagnostic::{
CandidateFailureDiagnostic, CandidateFailureDiagnosticKind,
};
pub(crate) use self::passthrough::{
build_local_same_format_stream_plan_and_reports, build_local_same_format_sync_plan_and_reports,
};
@@ -41,7 +32,7 @@ pub(crate) use self::plan_builders::{
build_openai_responses_stream_plan_from_decision,
build_openai_responses_sync_plan_from_decision, build_passthrough_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
AiStreamAttempt, AiSyncAttempt,
};
pub(crate) use self::route::is_matching_stream_request as planner_is_matching_stream_request;
pub(crate) use self::specialized::{
@@ -64,6 +55,11 @@ pub(crate) use self::state::{
GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth,
PlannerAppState,
};
pub(crate) use aether_ai_serving::extract_ai_pool_sticky_session_token as extract_pool_sticky_session_token;
pub(crate) use aether_ai_serving::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
CandidateFailureDiagnostic, CandidateFailureDiagnosticKind,
};
pub(crate) async fn maybe_build_sync_decision_payload(
state: &AppState,
@@ -73,7 +69,7 @@ pub(crate) async fn maybe_build_sync_decision_payload(
body_json: &serde_json::Value,
body_base64: Option<&str>,
body_is_empty: bool,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
decision::maybe_build_sync_decision_payload(
state,
parts,
@@ -93,7 +89,7 @@ pub(crate) async fn maybe_build_stream_decision_payload(
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
decision::maybe_build_stream_decision_payload(
state,
parts,
@@ -113,7 +109,7 @@ pub(crate) async fn maybe_build_sync_plan_payload(
body_json: &serde_json::Value,
body_base64: Option<&str>,
body_is_empty: bool,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
decision::maybe_build_sync_plan_payload_impl(
state,
parts,
@@ -133,7 +129,7 @@ pub(crate) async fn maybe_build_stream_plan_payload(
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> Result<Option<GatewayControlPlanResponse>, GatewayError> {
) -> Result<Option<AiExecutionPlanPayload>, GatewayError> {
decision::maybe_build_stream_plan_payload_impl(
state,
parts,

View File

@@ -8,6 +8,5 @@ pub(crate) use self::provider::{
maybe_build_local_same_format_provider_decision_payload_for_candidate,
maybe_build_stream_local_same_format_provider_decision_payload,
maybe_build_sync_local_same_format_provider_decision_payload,
resolve_same_format_provider_transport_unsupported_reason_for_trace,
};
pub(crate) use crate::ai_pipeline::transport::provider_types::provider_type_supports_local_same_format_transport;
pub(crate) use crate::ai_serving::transport::provider_types::provider_type_supports_local_same_format_transport;

View File

@@ -0,0 +1,96 @@
use aether_contracts::RequestBody;
use super::{
augment_sync_report_context, build_ai_execution_plan_from_decision,
resolve_ai_passthrough_sync_request_body, take_ai_decision_plan_core, take_non_empty_string,
AiExecutionPlanFromDecisionParts, AiStreamAttempt, AiSyncAttempt,
};
use crate::{AiExecutionDecision, GatewayError};
pub(crate) fn build_passthrough_sync_plan_from_decision(
parts: &http::request::Parts,
payload: AiExecutionDecision,
) -> Result<Option<AiSyncAttempt>, GatewayError> {
let mut payload = payload;
let Some(core) = take_ai_decision_plan_core(&mut payload) else {
return Ok(None);
};
let Some(upstream_url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
let ignored_provider_request_body = serde_json::Value::Null;
let report_context = augment_sync_report_context(
payload.report_context.take(),
&provider_request_headers,
&ignored_provider_request_body,
)?;
let request_body = resolve_ai_passthrough_sync_request_body(
payload.provider_request_body.take(),
payload.provider_request_body_base64.take(),
);
let provider_request_method = take_non_empty_string(&mut payload.provider_request_method);
let content_type = payload
.content_type
.take()
.or_else(|| provider_request_headers.get("content-type").cloned());
let plan = build_ai_execution_plan_from_decision(
&mut payload,
AiExecutionPlanFromDecisionParts {
core,
method: provider_request_method.unwrap_or_else(|| parts.method.to_string()),
url: upstream_url,
headers: provider_request_headers,
content_type,
body: request_body,
stream: false,
},
);
Ok(Some(AiSyncAttempt {
plan,
report_kind: payload.report_kind,
report_context,
}))
}
pub(crate) fn build_passthrough_stream_plan_from_decision(
parts: &http::request::Parts,
payload: AiExecutionDecision,
) -> Result<Option<AiStreamAttempt>, GatewayError> {
let mut payload = payload;
let Some(core) = take_ai_decision_plan_core(&mut payload) else {
return Ok(None);
};
let Some(upstream_url) = take_non_empty_string(&mut payload.upstream_url) else {
return Ok(None);
};
let provider_request_headers = std::mem::take(&mut payload.provider_request_headers);
let content_type = payload
.content_type
.take()
.or_else(|| provider_request_headers.get("content-type").cloned());
let plan = build_ai_execution_plan_from_decision(
&mut payload,
AiExecutionPlanFromDecisionParts {
core,
method: parts.method.to_string(),
url: upstream_url,
headers: provider_request_headers,
content_type,
body: RequestBody {
json_body: None,
body_bytes_b64: None,
body_ref: None,
},
stream: true,
},
);
Ok(Some(AiStreamAttempt {
plan,
report_kind: payload.report_kind,
report_context: payload.report_context,
}))
}

View File

@@ -11,53 +11,50 @@ use serde_json::{json, Value};
use tracing::warn;
use uuid::Uuid;
use crate::ai_pipeline::planner::common::{
use crate::ai_serving::planner::common::{
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
};
use crate::ai_pipeline::planner::plan_builders::{
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
};
use crate::ai_pipeline::transport::antigravity::{
use crate::ai_serving::planner::plan_builders::{AiStreamAttempt, AiSyncAttempt};
use crate::ai_serving::transport::antigravity::{
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
build_antigravity_v1internal_url, classify_local_antigravity_request_support,
AntigravityEnvelopeRequestType, AntigravityRequestEnvelopeSupport,
AntigravityRequestSideSupport, AntigravityRequestUrlAction,
};
use crate::ai_pipeline::transport::auth::{
use crate::ai_serving::transport::auth::{
build_openai_passthrough_headers, resolve_local_gemini_auth, resolve_local_standard_auth,
};
use crate::ai_pipeline::transport::claude_code::{
use crate::ai_serving::transport::claude_code::{
build_claude_code_messages_url, build_claude_code_passthrough_headers,
sanitize_claude_code_request_body, supports_local_claude_code_transport_with_network,
};
use crate::ai_pipeline::transport::kiro::{
use crate::ai_serving::transport::kiro::{
build_kiro_generate_assistant_response_url, build_kiro_provider_headers,
build_kiro_provider_request_body, supports_local_kiro_request_transport_with_network,
KIRO_ENVELOPE_NAME,
};
use crate::ai_pipeline::transport::policy::{
use crate::ai_serving::transport::policy::{
supports_local_gemini_transport_with_network, supports_local_standard_transport_with_network,
};
use crate::ai_pipeline::transport::url::{
use crate::ai_serving::transport::url::{
build_claude_messages_url, build_gemini_content_url, build_passthrough_path_url,
};
use crate::ai_pipeline::transport::vertex::{
use crate::ai_serving::transport::vertex::{
build_vertex_api_key_gemini_content_url, resolve_local_vertex_api_key_query_auth,
supports_local_vertex_api_key_gemini_transport_with_network,
};
use crate::ai_pipeline::transport::{
use crate::ai_serving::transport::{
apply_local_body_rules, apply_local_header_rules, build_passthrough_headers,
ensure_upstream_auth_header, resolve_transport_execution_timeouts,
resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile,
LocalResolvedOAuthRequestAuth,
};
use crate::ai_pipeline::{
use crate::ai_serving::{
collect_control_headers, ConversionMode, ExecutionStrategy, GatewayControlDecision,
};
use crate::clock::current_unix_secs;
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
GatewayError,
append_execution_contract_fields_to_value, AiExecutionDecision, AppState, GatewayError,
};
mod family;
@@ -67,9 +64,8 @@ mod request;
pub(crate) use self::family::{
materialize_local_same_format_provider_candidate_attempts,
maybe_build_local_same_format_provider_decision_payload_for_candidate,
resolve_local_same_format_provider_decision_input,
resolve_same_format_provider_transport_unsupported_reason_for_trace,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
resolve_local_same_format_provider_decision_input, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec,
};
pub(crate) use self::family::{
maybe_build_stream_local_same_format_provider_decision_payload,

View File

@@ -1,11 +1,11 @@
use crate::ai_pipeline::planner::common::extract_requested_model_from_request;
use crate::ai_pipeline::planner::runtime_miss::{
use crate::ai_serving::planner::common::extract_requested_model_from_request;
use crate::ai_serving::planner::runtime_miss::{
apply_local_runtime_candidate_evaluation_progress_preserving_candidate_signal,
apply_local_runtime_candidate_terminal_reason, set_local_runtime_miss_diagnostic_reason,
};
use crate::ai_pipeline::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_pipeline::GatewayControlDecision;
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
use crate::ai_serving::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_serving::GatewayControlDecision;
use crate::{AiExecutionDecision, AppState, GatewayError};
use super::super::plans::{resolve_stream_spec, resolve_sync_spec};
use super::candidates::{
@@ -21,7 +21,7 @@ pub(crate) async fn maybe_build_sync_local_same_format_provider_decision_payload
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(spec) = resolve_sync_spec(plan_kind) else {
return Ok(None);
};
@@ -88,7 +88,7 @@ pub(crate) async fn maybe_build_stream_local_same_format_provider_decision_paylo
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(spec) = resolve_stream_spec(plan_kind) else {
return Ok(None);
};

View File

@@ -1,30 +1,25 @@
use tracing::warn;
use crate::ai_pipeline::planner::candidate_materialization::{
persist_available_local_execution_candidates_with_context,
persist_skipped_local_execution_candidates_with_context,
remember_first_local_candidate_affinity,
use crate::ai_serving::planner::candidate_materialization::{
materialize_local_execution_candidates_with_serving, LocalCandidateResolutionMode,
};
use crate::ai_pipeline::planner::candidate_metadata::{
use crate::ai_serving::planner::candidate_metadata::{
build_local_execution_candidate_contract_metadata,
build_local_execution_candidate_contract_metadata_for_candidate,
LocalExecutionCandidateMetadataParts,
};
use crate::ai_pipeline::planner::candidate_resolution::{
extract_pool_sticky_session_token, resolve_and_rank_local_execution_candidates,
SkippedLocalExecutionCandidate,
};
use crate::ai_pipeline::planner::common::extract_requested_model_from_request;
use crate::ai_pipeline::planner::decision_input::{
use crate::ai_serving::planner::candidate_resolution::SkippedLocalExecutionCandidate;
use crate::ai_serving::planner::common::extract_requested_model_from_request;
use crate::ai_serving::planner::decision_input::{
build_local_requested_model_decision_input, resolve_local_authenticated_decision_input,
};
use crate::ai_pipeline::planner::materialization_policy::{
use crate::ai_serving::planner::materialization_policy::{
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
};
use crate::ai_pipeline::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_pipeline::{
resolve_local_decision_execution_runtime_auth_context, ConversionMode, ExecutionStrategy,
GatewayControlDecision, PlannerAppState,
use crate::ai_serving::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_serving::{
ai_local_execution_contract_for_formats, extract_pool_sticky_session_token,
resolve_local_decision_execution_runtime_auth_context, GatewayControlDecision, PlannerAppState,
};
use crate::clock::current_unix_secs;
use crate::{AppState, GatewayError};
@@ -107,63 +102,32 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
current_unix_secs(),
)
.await?;
let (candidates, skipped_candidates) = resolve_and_rank_local_execution_candidates(
let outcome = materialize_local_execution_candidates_with_serving(
planner_state,
candidates,
trace_id,
spec_metadata.api_format,
&input.requested_model,
Some(&input.requested_model),
Some(&input.auth_snapshot),
input.required_capabilities.as_ref(),
sticky_session_token.as_deref(),
)
.await;
let skipped_candidates = preselection_skipped
.into_iter()
.map(|item| SkippedLocalExecutionCandidate {
candidate: item.candidate,
skip_reason: item.skip_reason,
transport: None,
ranking: None,
extra_data: None,
})
.chain(skipped_candidates)
.map(|mut skipped_candidate| {
let provider_api_format = skipped_candidate
.transport
.as_ref()
.map(|transport| transport.endpoint.api_format.trim().to_ascii_lowercase())
.unwrap_or_else(|| spec_metadata.api_format.to_string());
skipped_candidate.extra_data = Some(
build_local_execution_candidate_contract_metadata_for_candidate(
&skipped_candidate.candidate,
skipped_candidate.transport_ref(),
provider_api_format.as_str(),
spec_metadata.api_format,
serde_json::Map::new(),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
provider_api_format.as_str(),
),
);
skipped_candidate
})
.collect::<Vec<_>>();
let candidate_count = candidates.len() + skipped_candidates.len();
remember_first_local_candidate_affinity(
planner_state,
Some(&input.auth_snapshot),
spec_metadata.api_format,
Some(&input.requested_model),
&candidates,
);
let available_candidate_count = candidates.len() as u32;
let attempts = persist_available_local_execution_candidates_with_context(
planner_state,
trace_id,
persistence_policy.available,
persistence_policy,
candidates,
preselection_skipped
.into_iter()
.map(|item| SkippedLocalExecutionCandidate {
candidate: item.candidate,
skip_reason: item.skip_reason,
transport: None,
ranking: None,
extra_data: None,
})
.collect(),
LocalCandidateResolutionMode::Standard,
|eligible| {
let (execution_strategy, conversion_mode) = ai_local_execution_contract_for_formats(
spec_metadata.api_format,
spec_metadata.api_format,
);
Some(build_local_execution_candidate_contract_metadata(
LocalExecutionCandidateMetadataParts {
eligible,
@@ -171,22 +135,37 @@ pub(crate) async fn materialize_local_same_format_provider_candidate_attempts(
client_api_format: spec_metadata.api_format,
extra_fields: serde_json::Map::new(),
},
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
execution_strategy,
conversion_mode,
spec_metadata.api_format,
))
},
|mut skipped_candidate| {
let provider_api_format = skipped_candidate
.transport
.as_ref()
.map(|transport| transport.endpoint.api_format.trim().to_ascii_lowercase())
.unwrap_or_else(|| spec_metadata.api_format.to_string());
let (execution_strategy, conversion_mode) = ai_local_execution_contract_for_formats(
spec_metadata.api_format,
provider_api_format.as_str(),
);
skipped_candidate.extra_data = Some(
build_local_execution_candidate_contract_metadata_for_candidate(
&skipped_candidate.candidate,
skipped_candidate.transport_ref(),
provider_api_format.as_str(),
spec_metadata.api_format,
serde_json::Map::new(),
execution_strategy,
conversion_mode,
provider_api_format.as_str(),
),
);
skipped_candidate
},
)
.await;
persist_skipped_local_execution_candidates_with_context(
state,
trace_id,
persistence_policy.skipped,
available_candidate_count,
skipped_candidates,
)
.await;
Ok((attempts, candidate_count))
Ok((outcome.attempts, outcome.candidate_count))
}

View File

@@ -12,7 +12,6 @@ pub(crate) use self::candidates::{
resolve_local_same_format_provider_decision_input,
};
pub(crate) use self::payload::maybe_build_local_same_format_provider_decision_payload_for_candidate;
pub(crate) use self::request::resolve_same_format_provider_transport_unsupported_reason_for_trace;
pub(crate) use crate::ai_pipeline::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalSameFormatProviderCandidateAttempt;
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalSameFormatProviderDecisionInput;
pub(crate) use crate::ai_pipeline::{LocalSameFormatProviderFamily, LocalSameFormatProviderSpec};
pub(crate) use crate::ai_serving::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalSameFormatProviderCandidateAttempt;
pub(crate) use crate::ai_serving::planner::decision_input::LocalRequestedModelDecisionInput as LocalSameFormatProviderDecisionInput;
pub(crate) use crate::ai_serving::{LocalSameFormatProviderFamily, LocalSameFormatProviderSpec};

View File

@@ -1,28 +1,28 @@
use serde_json::json;
use crate::ai_pipeline::planner::candidate_materialization::{
use crate::ai_serving::ai_local_execution_contract_for_formats;
use crate::ai_serving::build_request_trace_proxy_value;
use crate::ai_serving::planner::candidate_materialization::{
mark_skipped_local_execution_candidate, mark_skipped_local_execution_candidate_with_extra_data,
mark_skipped_local_execution_candidate_with_failure_diagnostic,
};
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
use crate::ai_pipeline::planner::materialization_policy::{
use crate::ai_serving::planner::materialization_policy::{
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
};
use crate::ai_pipeline::planner::payload_metadata::{
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
};
use crate::ai_pipeline::planner::report_context::{
use crate::ai_serving::planner::report_context::{
build_local_execution_report_context, LocalExecutionReportContextParts,
};
use crate::ai_pipeline::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_pipeline::planner::CandidateFailureDiagnostic;
use crate::ai_pipeline::transport::{
use crate::ai_serving::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_serving::planner::CandidateFailureDiagnostic;
use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
};
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy};
use crate::{
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value,
AiExecutionDecision, AppState,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
@@ -40,7 +40,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
input: &LocalSameFormatProviderDecisionInput,
attempt: LocalSameFormatProviderCandidateAttempt,
spec: LocalSameFormatProviderSpec,
) -> Option<GatewayControlSyncDecisionResponse> {
) -> Option<AiExecutionDecision> {
let spec_metadata = local_same_format_provider_spec_metadata(spec);
let LocalSameFormatProviderCandidateAttempt {
eligible,
@@ -49,6 +49,8 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
..
} = &attempt;
let candidate = &eligible.candidate;
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(spec_metadata.api_format, spec_metadata.api_format);
let resolved = resolve_local_same_format_provider_candidate_payload_parts(
state, parts, trace_id, body_json, input, &attempt, spec,
)
@@ -74,7 +76,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
if resolved.is_kiro {
extra_fields.insert(
"envelope_name".to_string(),
json!(crate::ai_pipeline::transport::kiro::KIRO_ENVELOPE_NAME),
json!(crate::ai_serving::transport::kiro::KIRO_ENVELOPE_NAME),
);
} else if resolved.is_antigravity {
extra_fields.insert(
@@ -109,7 +111,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
provider_request_method: Some(serde_json::Value::Null),
provider_request_headers: Some(&resolved.provider_request_headers),
original_headers: &parts.headers,
request_origin: Some(crate::ai_pipeline::request_origin_from_parts(parts)),
request_origin: Some(crate::ai_serving::request_origin_from_parts(parts)),
original_request_body_json: Some(body_json),
original_request_body_base64: None,
client_requested_stream: body_json
@@ -121,8 +123,8 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
needs_conversion: false,
extra_fields,
}),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
execution_strategy,
conversion_mode,
spec_metadata.api_format,
spec_metadata.api_format,
),
@@ -142,12 +144,12 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
provider_request_body,
} = resolved;
Some(build_local_execution_decision_response(
LocalExecutionDecisionResponseParts {
Some(build_ai_execution_decision_response(
AiExecutionDecisionResponseParts {
decision_is_stream: spec_metadata.require_streaming,
decision_kind: spec_metadata.decision_kind.to_string(),
execution_strategy: ExecutionStrategy::LocalSameFormat,
conversion_mode: ConversionMode::None,
execution_strategy,
conversion_mode,
request_id: trace_id.to_string(),
candidate_id: candidate_id.to_string(),
provider_name: transport.provider.name.clone(),

View File

@@ -3,18 +3,15 @@ use std::sync::Arc;
use serde_json::Value;
use crate::ai_pipeline::transport::antigravity::{
use crate::ai_serving::transport::antigravity::{
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
};
use crate::ai_pipeline::transport::auth::{
build_complete_passthrough_headers, build_complete_passthrough_headers_with_auth,
use crate::ai_serving::transport::{
build_same_format_provider_headers, SameFormatProviderHeadersInput,
};
use crate::ai_pipeline::transport::claude_code::build_claude_code_passthrough_headers;
use crate::ai_pipeline::transport::kiro::{build_kiro_provider_headers, KiroProviderHeadersInput};
use crate::ai_pipeline::transport::{apply_local_header_rules, ensure_upstream_auth_header};
use crate::ai_pipeline::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::AppState;
mod policy;
@@ -30,51 +27,7 @@ use super::{
LocalSameFormatProviderCandidateAttempt, LocalSameFormatProviderDecisionInput,
LocalSameFormatProviderSpec,
};
use crate::ai_pipeline::planner::standard::same_format_provider_request_body_failure_extra_data;
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> Option<&'static str> {
let provider_api_format =
match crate::ai_pipeline::normalize_api_format_alias(provider_api_format).as_str() {
"openai:chat" => "openai:chat",
"openai:responses" => "openai:responses",
"openai:responses:compact" => "openai:responses:compact",
"claude:messages" => "claude:messages",
"gemini:generate_content" => "gemini:generate_content",
_ => return Some("transport_api_format_unsupported"),
};
let behavior = policy::classify_same_format_provider_request_behavior(
transport,
crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata {
api_format: provider_api_format,
require_streaming: false,
requested_model_family: None,
decision_kind: "trace_candidate_metadata",
report_kind: Some("trace_candidate_metadata"),
},
);
if !behavior.is_antigravity
&& !behavior.is_claude_code
&& !behavior.is_vertex
&& !behavior.is_kiro
{
return None;
}
let family = if provider_api_format.starts_with("gemini:") {
crate::ai_pipeline::LocalSameFormatProviderFamily::Gemini
} else {
crate::ai_pipeline::LocalSameFormatProviderFamily::Standard
};
policy::same_format_provider_transport_unsupported_reason(
&behavior,
transport,
family,
provider_api_format,
)
}
use crate::ai_serving::planner::standard::same_format_provider_request_body_failure_extra_data;
pub(crate) struct LocalSameFormatProviderCandidatePayloadParts {
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
@@ -228,74 +181,28 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
return None;
};
let Some(provider_request_headers) = (if let Some(kiro_auth) = prepared.kiro_auth.as_ref() {
build_kiro_provider_headers(KiroProviderHeadersInput {
let extra_headers = antigravity_auth
.as_ref()
.map(build_antigravity_static_identity_headers)
.unwrap_or_default();
let Some(provider_request_headers) =
build_same_format_provider_headers(SameFormatProviderHeadersInput {
headers: &parts.headers,
provider_request_body: &provider_request_body,
original_request_body: body_json,
header_rules: prepared.transport.endpoint.header_rules.as_ref(),
auth_header: prepared.auth_header.as_deref().unwrap_or_default(),
auth_value: prepared.auth_value.as_deref().unwrap_or_default(),
auth_config: &kiro_auth.auth_config,
machine_id: kiro_auth.machine_id.as_str(),
behavior: prepared.behavior,
auth_header: prepared.auth_header.as_deref(),
auth_value: prepared.auth_value.as_deref(),
extra_headers: &extra_headers,
key_fingerprint: prepared.transport.key.fingerprint.as_ref(),
kiro_auth_config: prepared.kiro_auth.as_ref().map(|auth| &auth.auth_config),
kiro_machine_id: prepared
.kiro_auth
.as_ref()
.map(|auth| auth.machine_id.as_str()),
})
} else {
let extra_headers = antigravity_auth
.as_ref()
.map(build_antigravity_static_identity_headers)
.unwrap_or_default();
let mut provider_request_headers = if prepared.is_claude_code {
build_claude_code_passthrough_headers(
&parts.headers,
prepared.auth_header.as_deref().unwrap_or_default(),
prepared.auth_value.as_deref().unwrap_or_default(),
&extra_headers,
prepared.upstream_is_stream,
prepared.transport.key.fingerprint.as_ref(),
)
} else if prepared.is_vertex {
build_complete_passthrough_headers(
&parts.headers,
&extra_headers,
Some("application/json"),
)
} else {
build_complete_passthrough_headers_with_auth(
&parts.headers,
prepared.auth_header.as_deref().unwrap_or_default(),
prepared.auth_value.as_deref().unwrap_or_default(),
&extra_headers,
Some("application/json"),
)
};
let protected_headers = prepared
.auth_header
.as_deref()
.filter(|value| !value.trim().is_empty())
.map(|value| vec![value, "content-type"])
.unwrap_or_else(|| vec!["content-type"]);
if !apply_local_header_rules(
&mut provider_request_headers,
prepared.transport.endpoint.header_rules.as_ref(),
&protected_headers,
&provider_request_body,
Some(body_json),
) {
None
} else {
if let (Some(auth_header), Some(auth_value)) = (
prepared.auth_header.as_deref(),
prepared.auth_value.as_deref(),
) {
ensure_upstream_auth_header(&mut provider_request_headers, auth_header, auth_value);
}
if prepared.upstream_is_stream {
provider_request_headers
.insert("accept".to_string(), "text/event-stream".to_string());
}
Some(provider_request_headers)
}
}) else {
else {
mark_skipped_local_same_format_provider_candidate_with_failure_diagnostic(
state,
input,

View File

@@ -0,0 +1,86 @@
use crate::ai_serving::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata;
use crate::ai_serving::transport::{
classify_same_format_provider_request_behavior as classify_same_format_provider_request_behavior_impl,
resolve_same_format_provider_direct_auth as resolve_same_format_provider_direct_auth_impl,
same_format_provider_transport_supported as same_format_provider_transport_supported_impl,
same_format_provider_transport_unsupported_reason as same_format_provider_transport_unsupported_reason_impl,
should_try_same_format_provider_oauth_auth as should_try_same_format_provider_oauth_auth_impl,
GatewayProviderTransportSnapshot, SameFormatProviderFamily, SameFormatProviderRequestBehavior,
SameFormatProviderRequestBehaviorParams,
};
use super::super::LocalSameFormatProviderFamily;
pub(super) fn classify_same_format_provider_request_behavior(
transport: &GatewayProviderTransportSnapshot,
spec_metadata: LocalExecutionSurfaceSpecMetadata,
) -> SameFormatProviderRequestBehavior {
classify_same_format_provider_request_behavior_impl(
transport,
SameFormatProviderRequestBehaviorParams {
require_streaming: spec_metadata.require_streaming,
report_kind: spec_metadata
.report_kind
.expect("same-format provider specs should declare report kind"),
},
)
}
pub(super) fn same_format_provider_transport_supported(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> bool {
same_format_provider_transport_supported_impl(
behavior,
transport,
same_format_provider_family(family),
api_format,
)
}
pub(super) fn same_format_provider_transport_unsupported_reason(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> Option<&'static str> {
same_format_provider_transport_unsupported_reason_impl(
behavior,
transport,
same_format_provider_family(family),
api_format,
)
}
pub(super) fn should_try_same_format_provider_oauth_auth(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
) -> bool {
should_try_same_format_provider_oauth_auth_impl(
behavior,
transport,
same_format_provider_family(family),
)
}
pub(super) fn resolve_same_format_provider_direct_auth(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
) -> Option<(String, String)> {
resolve_same_format_provider_direct_auth_impl(
behavior,
transport,
same_format_provider_family(family),
)
}
fn same_format_provider_family(family: LocalSameFormatProviderFamily) -> SameFormatProviderFamily {
match family {
LocalSameFormatProviderFamily::Standard => SameFormatProviderFamily::Standard,
LocalSameFormatProviderFamily::Gemini => SameFormatProviderFamily::Gemini,
}
}

View File

@@ -1,13 +1,14 @@
use std::sync::Arc;
use crate::ai_pipeline::planner::candidate_preparation::{
use crate::ai_serving::planner::candidate_preparation::{
resolve_candidate_mapped_model, resolve_candidate_oauth_auth, OauthPreparationContext,
};
use crate::ai_pipeline::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_pipeline::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_pipeline::transport::kiro::KiroRequestAuth;
use crate::ai_pipeline::transport::vertex::resolve_local_vertex_api_key_query_auth;
use crate::ai_pipeline::{
use crate::ai_serving::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_serving::planner::spec_metadata::local_same_format_provider_spec_metadata;
use crate::ai_serving::transport::kiro::KiroRequestAuth;
use crate::ai_serving::transport::vertex::resolve_local_vertex_api_key_query_auth;
use crate::ai_serving::transport::SameFormatProviderRequestBehavior;
use crate::ai_serving::{
GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth, PlannerAppState,
};
use crate::AppState;
@@ -22,6 +23,7 @@ use super::policy::{
pub(super) struct PreparedSameFormatProviderCandidate {
pub(super) transport: Arc<GatewayProviderTransportSnapshot>,
pub(super) behavior: SameFormatProviderRequestBehavior,
pub(super) is_antigravity: bool,
pub(super) is_claude_code: bool,
pub(super) is_vertex: bool,
@@ -158,6 +160,7 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
Some(PreparedSameFormatProviderCandidate {
transport,
behavior,
is_antigravity: behavior.is_antigravity,
is_claude_code: behavior.is_claude_code,
is_vertex: behavior.is_vertex,

View File

@@ -1,15 +1,15 @@
use tracing::warn;
use crate::ai_pipeline::planner::common::extract_requested_model_from_request;
use crate::ai_pipeline::planner::runtime_miss::{
use crate::ai_serving::planner::common::extract_requested_model_from_request;
use crate::ai_serving::planner::runtime_miss::{
apply_local_runtime_candidate_evaluation_progress_preserving_candidate_signal,
apply_local_runtime_candidate_terminal_reason, set_local_runtime_miss_diagnostic_reason,
};
use crate::ai_pipeline::planner::spec_metadata::{
use crate::ai_serving::planner::spec_metadata::{
build_stream_plan_from_requested_model_family, build_sync_plan_from_requested_model_family,
local_same_format_provider_spec_metadata,
};
pub(crate) use crate::ai_pipeline::{
pub(crate) use crate::ai_serving::{
resolve_local_same_format_stream_spec as resolve_stream_spec,
resolve_local_same_format_sync_spec as resolve_sync_spec,
};
@@ -17,8 +17,8 @@ pub(crate) use crate::ai_pipeline::{
use super::{
materialize_local_same_format_provider_candidate_attempts,
maybe_build_local_same_format_provider_decision_payload_for_candidate,
resolve_local_same_format_provider_decision_input, AppState, GatewayControlDecision,
GatewayError, LocalSameFormatProviderSpec, LocalStreamPlanAndReport, LocalSyncPlanAndReport,
resolve_local_same_format_provider_decision_input, AiStreamAttempt, AiSyncAttempt, AppState,
GatewayControlDecision, GatewayError, LocalSameFormatProviderSpec,
};
pub(crate) async fn build_local_sync_plan_and_reports(
@@ -28,7 +28,7 @@ pub(crate) async fn build_local_sync_plan_and_reports(
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
spec: LocalSameFormatProviderSpec,
) -> Result<Vec<LocalSyncPlanAndReport>, GatewayError> {
) -> Result<Vec<AiSyncAttempt>, GatewayError> {
let spec_metadata = local_same_format_provider_spec_metadata(spec);
let requested_model_family = spec_metadata
.requested_model_family
@@ -113,7 +113,7 @@ pub(crate) async fn build_local_stream_plan_and_reports(
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
spec: LocalSameFormatProviderSpec,
) -> Result<Vec<LocalStreamPlanAndReport>, GatewayError> {
) -> Result<Vec<AiStreamAttempt>, GatewayError> {
let spec_metadata = local_same_format_provider_spec_metadata(spec);
let requested_model_family = spec_metadata
.requested_model_family

View File

@@ -0,0 +1,36 @@
use serde_json::Value;
use super::super::LocalSameFormatProviderSpec;
use crate::ai_serving::transport::{
build_same_format_provider_request_body as build_same_format_provider_request_body_impl,
SameFormatProviderFamily, SameFormatProviderRequestBodyInput,
};
pub(crate) fn build_same_format_provider_request_body(
body_json: &Value,
mapped_model: &str,
spec: LocalSameFormatProviderSpec,
body_rules: Option<&Value>,
upstream_is_stream: bool,
kiro_auth: Option<&crate::ai_serving::transport::kiro::KiroRequestAuth>,
is_claude_code: bool,
) -> Option<Value> {
build_same_format_provider_request_body_impl(SameFormatProviderRequestBodyInput {
body_json,
mapped_model,
family: same_format_provider_family(spec.family),
body_rules,
upstream_is_stream,
kiro_auth_config: kiro_auth.map(|auth| &auth.auth_config),
is_claude_code,
})
}
fn same_format_provider_family(
family: super::super::LocalSameFormatProviderFamily,
) -> SameFormatProviderFamily {
match family {
super::super::LocalSameFormatProviderFamily::Standard => SameFormatProviderFamily::Standard,
super::super::LocalSameFormatProviderFamily::Gemini => SameFormatProviderFamily::Gemini,
}
}

View File

@@ -0,0 +1,27 @@
use crate::ai_serving::transport::{
build_same_format_provider_upstream_url as build_same_format_provider_upstream_url_impl,
SameFormatProviderUpstreamUrlParams,
};
use crate::ai_serving::GatewayProviderTransportSnapshot;
use super::super::LocalSameFormatProviderSpec;
pub(crate) fn build_same_format_upstream_url(
parts: &http::request::Parts,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
spec: LocalSameFormatProviderSpec,
upstream_is_stream: bool,
kiro_auth: Option<&crate::ai_serving::transport::kiro::KiroRequestAuth>,
) -> Option<String> {
build_same_format_provider_upstream_url_impl(
transport,
SameFormatProviderUpstreamUrlParams {
provider_api_format: spec.api_format,
mapped_model,
upstream_is_stream,
request_query: parts.uri.query(),
kiro_api_region: kiro_auth.map(|auth| auth.auth_config.effective_api_region()),
},
)
}

View File

@@ -1,9 +1,16 @@
use std::collections::BTreeMap;
use crate::ai_pipeline::augment_sync_report_context as augment_sync_report_context_impl;
pub(crate) use crate::ai_pipeline::contracts::generic_decision_missing_exact_provider_request;
pub(crate) use crate::ai_pipeline::{LocalStreamPlanAndReport, LocalSyncPlanAndReport};
use crate::{GatewayControlSyncDecisionResponse, GatewayError};
pub(crate) use aether_ai_serving::{
build_ai_execution_plan_from_decision, resolve_ai_passthrough_sync_request_body,
take_ai_decision_plan_core, take_ai_non_empty_string as take_non_empty_string,
take_ai_upstream_auth_pair, AiExecutionPlanFromDecisionParts,
};
use crate::ai_serving::augment_sync_report_context as augment_sync_report_context_impl;
pub(crate) use crate::ai_serving::{
generic_decision_missing_exact_provider_request, AiStreamAttempt, AiSyncAttempt,
};
use crate::{AiExecutionDecision, GatewayError};
#[path = "standard/gemini/plan_builders.rs"]
mod gemini_builders;
@@ -41,7 +48,3 @@ pub(super) fn augment_sync_report_context(
)
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(super) fn take_non_empty_string(value: &mut Option<String>) -> Option<String> {
value.take().filter(|value| !value.trim().is_empty())
}

View File

@@ -1,21 +1,24 @@
use std::cmp::Ordering;
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use aether_ai_serving::{
run_ai_pool_scheduler, AiPoolCandidateFacts, AiPoolCandidateInput,
AiPoolCandidateOrchestration, AiPoolCatalogKeyContext, AiPoolRuntimeState,
AiPoolSchedulingConfig, AiPoolSchedulingPreset,
};
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
use serde_json::{Map, Value};
use tracing::warn;
use crate::ai_pipeline::planner::candidate_resolution::{
use crate::ai_serving::planner::candidate_resolution::{
EligibleLocalExecutionCandidate, SkippedLocalExecutionCandidate,
};
use crate::ai_pipeline::PlannerAppState;
use crate::ai_serving::PlannerAppState;
use crate::clock::current_unix_ms;
use crate::handlers::shared::provider_pool::admin_provider_pool_config_from_config_value;
use crate::handlers::shared::provider_pool::read_admin_provider_pool_runtime_state;
use crate::handlers::shared::provider_pool::{
AdminProviderPoolConfig, AdminProviderPoolRuntimeState, AdminProviderPoolSchedulingPreset,
AdminProviderPoolConfig, AdminProviderPoolRuntimeState,
};
use crate::handlers::shared::{
parse_catalog_auth_config_json, provider_key_health_summary,
@@ -24,39 +27,9 @@ use crate::handlers::shared::{
use crate::orchestration::LocalExecutionCandidateMetadata;
use crate::provider_key_auth::provider_key_auth_semantics;
const POOL_ACCOUNT_BLOCKED_SKIP_REASON: &str = "pool_account_blocked";
const POOL_ACCOUNT_EXHAUSTED_SKIP_REASON: &str = "pool_account_exhausted";
const POOL_COOLDOWN_SKIP_REASON: &str = "pool_cooldown";
const POOL_COST_LIMIT_REACHED_SKIP_REASON: &str = "pool_cost_limit_reached";
static LOAD_BALANCE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct PoolGroupKey {
provider_id: String,
endpoint_id: String,
model_id: String,
selected_provider_model_name: String,
provider_api_format: String,
singleton_key_id: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
struct PoolCatalogKeyContext {
oauth_plan_type: Option<String>,
quota_usage_ratio: Option<f64>,
quota_reset_seconds: Option<f64>,
account_blocked: bool,
quota_exhausted: bool,
health_score: Option<f64>,
latency_avg_ms: Option<f64>,
catalog_lru_score: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct NormalizedPoolPreset {
preset: String,
mode: Option<String>,
}
type PoolCatalogKeyContext = AiPoolCatalogKeyContext;
pub(crate) async fn apply_local_execution_pool_scheduler(
state: PlannerAppState<'_>,
@@ -319,80 +292,45 @@ fn apply_local_execution_pool_scheduler_with_runtime_map(
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let mut group_order = Vec::new();
let mut groups = BTreeMap::<PoolGroupKey, Vec<EligibleLocalExecutionCandidate>>::new();
for candidate in candidates {
let pool_enabled = pool_config_for_candidate(&candidate).is_some();
let group_key = pool_group_key(&candidate, pool_enabled);
match groups.entry(group_key) {
Entry::Vacant(entry) => {
group_order.push(entry.key().clone());
entry.insert(vec![candidate]);
let runtime_by_provider = runtime_by_provider
.iter()
.map(|(provider_id, runtime)| (provider_id.clone(), ai_pool_runtime_state(runtime)))
.collect::<BTreeMap<_, _>>();
let inputs = candidates
.into_iter()
.map(|candidate| {
let key_context = key_context_by_id
.get(&candidate.candidate.key_id)
.cloned()
.unwrap_or_default();
AiPoolCandidateInput {
facts: ai_pool_candidate_facts(&candidate),
pool_config: pool_config_for_candidate(&candidate).map(ai_pool_scheduling_config),
key_context,
candidate,
}
Entry::Occupied(mut entry) => {
entry.get_mut().push(candidate);
}
}
}
})
.collect::<Vec<_>>();
let outcome = run_ai_pool_scheduler(inputs, &runtime_by_provider, pool_sort_seed().as_str());
let mut reordered = Vec::new();
let mut skipped = Vec::new();
let default_runtime = AdminProviderPoolRuntimeState::default();
let candidates = outcome
.candidates
.into_iter()
.map(|scheduled| apply_ai_pool_orchestration(scheduled.candidate, scheduled.orchestration))
.collect::<Vec<_>>();
let skipped_candidates = outcome
.skipped_candidates
.into_iter()
.map(|skipped| SkippedLocalExecutionCandidate {
candidate: skipped.candidate.candidate,
skip_reason: skipped.skip_reason,
transport: Some(skipped.candidate.transport),
ranking: skipped.candidate.ranking,
extra_data: None,
})
.collect::<Vec<_>>();
for group_key in group_order {
let Some(group) = groups.remove(&group_key) else {
continue;
};
let candidate_group_id = local_execution_candidate_group_id(&group_key);
let Some(pool_config) =
pool_config_for_candidate(group.first().expect("group should exist"))
else {
reordered.extend(annotate_local_execution_group_candidates(
group,
candidate_group_id.as_str(),
false,
));
continue;
};
let runtime = runtime_by_provider
.get(&group_key.provider_id)
.unwrap_or(&default_runtime);
let (group_candidates, group_skipped) = schedule_pool_group(
group,
pool_config,
runtime,
key_context_by_id,
candidate_group_id.as_str(),
);
reordered.extend(group_candidates);
skipped.extend(group_skipped);
}
(reordered, skipped)
}
fn pool_group_key(candidate: &EligibleLocalExecutionCandidate, pool_enabled: bool) -> PoolGroupKey {
PoolGroupKey {
provider_id: candidate.candidate.provider_id.clone(),
endpoint_id: candidate.candidate.endpoint_id.clone(),
model_id: candidate.candidate.model_id.clone(),
selected_provider_model_name: candidate.candidate.selected_provider_model_name.clone(),
provider_api_format: candidate.provider_api_format.clone(),
singleton_key_id: (!pool_enabled).then(|| candidate.candidate.key_id.clone()),
}
}
fn local_execution_candidate_group_id(group_key: &PoolGroupKey) -> String {
format!(
"provider={}|endpoint={}|model={}|selected_model={}|api_format={}|singleton_key={}",
group_key.provider_id,
group_key.endpoint_id,
group_key.model_id,
group_key.selected_provider_model_name,
group_key.provider_api_format,
group_key.singleton_key_id.as_deref().unwrap_or("*"),
)
(candidates, skipped_candidates)
}
fn pool_config_for_candidate(
@@ -401,659 +339,76 @@ fn pool_config_for_candidate(
admin_provider_pool_config_from_config_value(candidate.transport.provider.config.as_ref())
}
fn schedule_pool_group(
group: Vec<EligibleLocalExecutionCandidate>,
pool_config: AdminProviderPoolConfig,
runtime: &AdminProviderPoolRuntimeState,
key_context_by_id: &BTreeMap<String, PoolCatalogKeyContext>,
candidate_group_id: &str,
) -> (
Vec<EligibleLocalExecutionCandidate>,
Vec<SkippedLocalExecutionCandidate>,
) {
let provider_type = group
.first()
.map(|candidate| {
candidate
.transport
.provider
.provider_type
.trim()
.to_ascii_lowercase()
})
.unwrap_or_default();
let active_presets =
normalize_enabled_pool_presets(&pool_config.scheduling_presets, provider_type.as_str());
let mut available = Vec::new();
let mut skipped = Vec::new();
for (original_index, eligible) in group.into_iter().enumerate() {
let EligibleLocalExecutionCandidate {
candidate,
transport,
provider_api_format,
orchestration,
ranking,
} = eligible;
let key_id = candidate.key_id.clone();
let mut key_context = key_context_by_id.get(&key_id).cloned().unwrap_or_default();
key_context.latency_avg_ms = runtime
.latency_avg_ms_by_key
.get(&key_id)
.copied()
.or(key_context.latency_avg_ms);
if key_context.account_blocked {
skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason: POOL_ACCOUNT_BLOCKED_SKIP_REASON,
transport: Some(transport),
ranking,
extra_data: None,
});
continue;
}
if pool_config.skip_exhausted_accounts && key_context.quota_exhausted {
skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason: POOL_ACCOUNT_EXHAUSTED_SKIP_REASON,
transport: Some(transport),
ranking,
extra_data: None,
});
continue;
}
if runtime.cooldown_reason_by_key.contains_key(&key_id) {
skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason: POOL_COOLDOWN_SKIP_REASON,
transport: Some(transport),
ranking,
extra_data: None,
});
continue;
}
if pool_config
.cost_limit_per_key_tokens
.is_some_and(|limit| runtime_cost_usage(runtime, key_id.as_str()) >= limit)
{
skipped.push(SkippedLocalExecutionCandidate {
candidate,
skip_reason: POOL_COST_LIMIT_REACHED_SKIP_REASON,
transport: Some(transport),
ranking,
extra_data: None,
});
continue;
}
let lru_score =
runtime_lru_score(runtime, key_id.as_str()).or(key_context.catalog_lru_score);
available.push(PoolGroupCandidateOrdering {
eligible: EligibleLocalExecutionCandidate {
candidate,
transport,
provider_api_format,
orchestration,
ranking,
},
key_context,
original_index,
lru_score,
cost_usage: runtime_cost_usage(runtime, key_id.as_str()),
});
}
if available.is_empty() {
return (Vec::new(), skipped);
}
let sticky_candidate = runtime
.sticky_bound_key_id
.as_ref()
.and_then(|sticky_key_id| {
available
.iter()
.position(|item| item.eligible.candidate.key_id == *sticky_key_id)
})
.map(|index| available.remove(index));
if !active_presets.is_empty() {
let sort_vectors = build_pool_sort_vectors(
&available,
&active_presets,
pool_config.lru_enabled,
group_sort_seed(
provider_type.as_str(),
available.first().map(|item| &item.eligible.candidate),
)
.as_str(),
pool_config.cost_limit_per_key_tokens,
);
available.sort_by(|left, right| {
sort_vectors
.get(&left.eligible.candidate.key_id)
.cmp(&sort_vectors.get(&right.eligible.candidate.key_id))
.then(left.original_index.cmp(&right.original_index))
});
} else if pool_config.lru_enabled {
let lru_ranks = lru_rank_indices(&available, false);
available.sort_by(|left, right| {
lru_ranks
.get(&left.eligible.candidate.key_id)
.cmp(&lru_ranks.get(&right.eligible.candidate.key_id))
.then(left.original_index.cmp(&right.original_index))
});
}
let mut ordered = Vec::new();
if let Some(sticky_candidate) = sticky_candidate {
ordered.push(sticky_candidate.eligible);
}
ordered.extend(available.into_iter().map(|item| item.eligible));
(
annotate_local_execution_group_candidates(ordered, candidate_group_id, true),
skipped,
)
}
fn annotate_local_execution_group_candidates(
candidates: Vec<EligibleLocalExecutionCandidate>,
candidate_group_id: &str,
pool_enabled: bool,
) -> Vec<EligibleLocalExecutionCandidate> {
candidates
.into_iter()
.enumerate()
.map(|(index, mut candidate)| {
candidate.orchestration = LocalExecutionCandidateMetadata {
candidate_group_id: Some(candidate_group_id.to_string()),
pool_key_index: pool_enabled.then_some(index as u32),
};
candidate
})
.collect()
}
#[derive(Debug)]
struct PoolGroupCandidateOrdering {
eligible: EligibleLocalExecutionCandidate,
key_context: PoolCatalogKeyContext,
original_index: usize,
lru_score: Option<f64>,
cost_usage: u64,
}
fn build_pool_sort_vectors(
items: &[PoolGroupCandidateOrdering],
presets: &[NormalizedPoolPreset],
lru_enabled: bool,
load_balance_seed: &str,
cost_limit_per_key_tokens: Option<u64>,
) -> BTreeMap<String, Vec<usize>> {
let mut vectors = BTreeMap::<String, Vec<usize>>::new();
let lru_ranks = lru_rank_indices(items, false);
let cache_affinity_ranks = lru_rank_indices(items, true);
for preset in presets {
let ranks = match preset.preset.as_str() {
"cache_affinity" => cache_affinity_ranks.clone(),
"priority_first" => priority_first_ranks(items, &lru_ranks),
"single_account" => single_account_ranks(items),
"plus_first" => plan_ranks(items, &lru_ranks, Some("plus_only")),
"pro_first" => plan_ranks(items, &lru_ranks, Some("pro_only")),
"free_first" => plan_ranks(items, &lru_ranks, Some("free_only")),
"team_first" => plan_ranks(items, &lru_ranks, Some("team_only")),
"health_first" => health_first_ranks(items, &lru_ranks),
"latency_first" => latency_first_ranks(items, &lru_ranks),
"cost_first" => cost_first_ranks(items, &lru_ranks, cost_limit_per_key_tokens),
"quota_balanced" => quota_balanced_ranks(items, &lru_ranks, cost_limit_per_key_tokens),
"recent_refresh" => recent_refresh_ranks(items, &lru_ranks),
"load_balance" => load_balance_ranks(items, load_balance_seed),
_ => continue,
};
for item in items {
let key_id = item.eligible.candidate.key_id.clone();
vectors
.entry(key_id.clone())
.or_default()
.push(*ranks.get(&key_id).unwrap_or(&0));
}
}
if lru_enabled {
for item in items {
let key_id = item.eligible.candidate.key_id.clone();
vectors
.entry(key_id.clone())
.or_default()
.push(*lru_ranks.get(&key_id).unwrap_or(&0));
}
}
vectors
}
fn lru_rank_indices(
items: &[PoolGroupCandidateOrdering],
descending: bool,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| item.lru_score);
rank_indices_from_score_map(items, &scores, descending)
}
fn priority_first_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| {
Some(f64::from(item.eligible.candidate.key_internal_priority))
});
if !score_map_has_variation(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn single_account_ranks(items: &[PoolGroupCandidateOrdering]) -> BTreeMap<String, usize> {
let n = items.len().saturating_sub(1).max(1) as f64;
let priority_scores = collect_metric_scores(items, |item| {
Some(f64::from(item.eligible.candidate.key_internal_priority))
});
let priority_ranks = rank_indices_from_score_map(items, &priority_scores, false);
let lru_desc_ranks = lru_rank_indices(items, true);
let combined_scores = items
.iter()
.map(|item| {
let key_id = item.eligible.candidate.key_id.clone();
let priority_rank = *priority_ranks.get(&key_id).unwrap_or(&0) as f64 / n;
let lru_rank = *lru_desc_ranks.get(&key_id).unwrap_or(&0) as f64 / n;
(key_id, Some((priority_rank * 0.75) + (lru_rank * 0.25)))
})
.collect::<BTreeMap<_, _>>();
rank_indices_from_score_map(items, &combined_scores, false)
}
fn plan_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
mode: Option<&str>,
) -> BTreeMap<String, usize> {
let scores = items
.iter()
.map(|item| {
(
item.eligible.candidate.key_id.clone(),
Some(plan_priority_score(
item.key_context.oauth_plan_type.as_deref(),
mode,
)),
)
})
.collect::<BTreeMap<_, _>>();
if !score_map_has_variation(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn health_first_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| {
item.key_context
.health_score
.map(|score| 1.0 - score.clamp(0.0, 1.0))
});
if !score_map_has_signal(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn latency_first_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| item.key_context.latency_avg_ms);
if !score_map_has_signal(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn cost_first_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
cost_limit_per_key_tokens: Option<u64>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| {
cost_penalty(item, cost_limit_per_key_tokens).or(item.key_context.quota_usage_ratio)
});
if !score_map_has_signal(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn quota_balanced_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
cost_limit_per_key_tokens: Option<u64>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| {
item.key_context
.quota_usage_ratio
.or_else(|| cost_penalty(item, cost_limit_per_key_tokens))
});
if !score_map_has_signal(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn recent_refresh_ranks(
items: &[PoolGroupCandidateOrdering],
lru_ranks: &BTreeMap<String, usize>,
) -> BTreeMap<String, usize> {
let scores = collect_metric_scores(items, |item| item.key_context.quota_reset_seconds);
if !score_map_has_signal(&scores) {
return lru_ranks.clone();
}
rank_indices_from_score_map(items, &scores, false)
}
fn load_balance_ranks(
items: &[PoolGroupCandidateOrdering],
load_balance_seed: &str,
) -> BTreeMap<String, usize> {
let scores = items
.iter()
.map(|item| {
let key_id = item.eligible.candidate.key_id.clone();
(
key_id.clone(),
Some(stable_hash_score(
format!("{load_balance_seed}:{key_id}").as_str(),
)),
)
})
.collect::<BTreeMap<_, _>>();
rank_indices_from_score_map(items, &scores, false)
}
fn group_sort_seed(
provider_type: &str,
candidate: Option<&aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate>,
) -> String {
fn pool_sort_seed() -> String {
let now_ms = current_unix_ms();
let sequence = LOAD_BALANCE_SEQUENCE.fetch_add(1, AtomicOrdering::Relaxed);
match candidate {
Some(candidate) => format!(
"{provider_type}:{}:{}:{}:{}:{now_ms}:{sequence}",
candidate.provider_id,
candidate.endpoint_id,
candidate.model_id,
candidate.selected_provider_model_name,
),
None => format!("{provider_type}:{now_ms}:{sequence}"),
format!("{now_ms}:{sequence}")
}
fn ai_pool_candidate_facts(candidate: &EligibleLocalExecutionCandidate) -> AiPoolCandidateFacts {
AiPoolCandidateFacts {
provider_id: candidate.candidate.provider_id.clone(),
endpoint_id: candidate.candidate.endpoint_id.clone(),
model_id: candidate.candidate.model_id.clone(),
selected_provider_model_name: candidate.candidate.selected_provider_model_name.clone(),
provider_api_format: candidate.provider_api_format.clone(),
provider_type: candidate.transport.provider.provider_type.clone(),
key_id: candidate.candidate.key_id.clone(),
key_internal_priority: candidate.candidate.key_internal_priority,
}
}
fn stable_hash_score(seed: &str) -> f64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
seed.hash(&mut hasher);
let value = hasher.finish();
value as f64 / u64::MAX as f64
}
fn collect_metric_scores<F>(
items: &[PoolGroupCandidateOrdering],
mut score_for: F,
) -> BTreeMap<String, Option<f64>>
where
F: FnMut(&PoolGroupCandidateOrdering) -> Option<f64>,
{
items
.iter()
.map(|item| (item.eligible.candidate.key_id.clone(), score_for(item)))
.collect()
}
fn score_map_has_signal(scores: &BTreeMap<String, Option<f64>>) -> bool {
scores.values().flatten().any(|value| value.is_finite())
}
fn score_map_has_variation(scores: &BTreeMap<String, Option<f64>>) -> bool {
let mut values = scores
.values()
.flatten()
.filter(|value| value.is_finite())
.map(|value| value.to_bits())
.collect::<BTreeSet<_>>();
values.len() > 1
}
fn rank_indices_from_score_map(
items: &[PoolGroupCandidateOrdering],
scores: &BTreeMap<String, Option<f64>>,
descending: bool,
) -> BTreeMap<String, usize> {
if !score_map_has_signal(scores) {
return items
.iter()
.map(|item| (item.eligible.candidate.key_id.clone(), 0))
.collect();
}
let mut decorated = items
.iter()
.map(|item| {
let key_id = item.eligible.candidate.key_id.clone();
let score = scores
.get(&key_id)
.copied()
.flatten()
.filter(|value| value.is_finite());
let sortable = score.map(|value| if descending { -value } else { value });
(
score.is_none(),
sortable.unwrap_or(f64::INFINITY),
item.original_index,
key_id,
)
})
.collect::<Vec<_>>();
decorated.sort_by(|left, right| {
left.0
.cmp(&right.0)
.then_with(|| left.1.partial_cmp(&right.1).unwrap_or(Ordering::Equal))
.then(left.2.cmp(&right.2))
});
decorated
.into_iter()
.enumerate()
.map(|(rank, (_, _, _, key_id))| (key_id, rank))
.collect()
}
fn cost_penalty(
item: &PoolGroupCandidateOrdering,
cost_limit_per_key_tokens: Option<u64>,
) -> Option<f64> {
if item.cost_usage == 0 {
return None;
}
if let Some(limit) = cost_limit_per_key_tokens.filter(|limit| *limit > 0) {
return Some((item.cost_usage as f64 / limit as f64).clamp(0.0, 1.0));
}
let used = item.cost_usage as f64;
Some((used / (used + 10_000.0)).clamp(0.0, 1.0))
}
fn plan_priority_score(plan_type: Option<&str>, mode: Option<&str>) -> f64 {
match mode.unwrap_or("both").trim().to_ascii_lowercase().as_str() {
"free_only" => match plan_type {
Some("free") => 0.0,
Some("team") => 0.5,
Some("enterprise" | "business") => 0.2,
Some("plus" | "pro") => 0.6,
Some(_) => 0.7,
None => 0.8,
},
"team_only" => match plan_type {
Some("team") => 0.0,
Some("free") => 0.5,
Some("enterprise" | "business") => 0.2,
Some("plus" | "pro") => 0.6,
Some(_) => 0.7,
None => 0.8,
},
"plus_only" => match plan_type {
Some("plus" | "pro") => 0.0,
Some("enterprise" | "business") => 0.3,
Some("free" | "team") => 0.7,
Some(_) => 0.7,
None => 0.8,
},
"pro_only" => match plan_type {
Some("pro") => 0.0,
Some("plus") => 0.3,
Some("enterprise" | "business") => 0.4,
Some("free" | "team") => 0.7,
Some(_) => 0.7,
None => 0.8,
},
_ => match plan_type {
Some("free" | "team") => 0.0,
Some("enterprise" | "business") => 0.2,
Some("plus" | "pro") => 0.6,
Some(_) => 0.7,
None => 0.8,
},
fn ai_pool_scheduling_config(config: AdminProviderPoolConfig) -> AiPoolSchedulingConfig {
AiPoolSchedulingConfig {
scheduling_presets: config
.scheduling_presets
.into_iter()
.map(|preset| AiPoolSchedulingPreset {
preset: preset.preset,
enabled: preset.enabled,
mode: preset.mode,
})
.collect(),
lru_enabled: config.lru_enabled,
skip_exhausted_accounts: config.skip_exhausted_accounts,
cost_limit_per_key_tokens: config.cost_limit_per_key_tokens,
}
}
fn normalize_enabled_pool_presets(
scheduling_presets: &[AdminProviderPoolSchedulingPreset],
provider_type: &str,
) -> Vec<NormalizedPoolPreset> {
let provider_type = provider_type.trim().to_ascii_lowercase();
let mut entries = Vec::<(usize, String, bool, Option<String>)>::new();
let mut seen = BTreeSet::new();
for (index, item) in scheduling_presets.iter().enumerate() {
let preset = item.preset.trim().to_ascii_lowercase();
if preset.is_empty() || !seen.insert(preset.clone()) {
continue;
}
entries.push((index, preset, item.enabled, item.mode.clone()));
}
if provider_type == "codex"
&& !entries.is_empty()
&& entries
.iter()
.all(|(_, preset, _, _)| preset != "recent_refresh")
{
entries.push((entries.len(), "recent_refresh".to_string(), true, None));
}
let mut group_anchor_index = BTreeMap::<String, usize>::new();
for (index, preset, _, _) in &entries {
let Some(mutex_group) = pool_preset_mutex_group(preset) else {
continue;
};
group_anchor_index
.entry(mutex_group.to_string())
.or_insert(*index);
}
let mut ordered_enabled = Vec::<(usize, usize, String, Option<String>)>::new();
let mut group_enabled = BTreeMap::<String, (usize, usize, String, Option<String>)>::new();
for (index, preset, enabled, mode) in entries {
if !enabled
|| preset == "lru"
|| !pool_preset_supported_for_provider(&preset, &provider_type)
{
continue;
}
let Some(mutex_group) = pool_preset_mutex_group(&preset) else {
ordered_enabled.push((index, index, preset, mode));
continue;
};
let anchor = group_anchor_index
.get(mutex_group)
.copied()
.unwrap_or(index);
let existing = group_enabled.get(mutex_group);
if existing.is_none_or(|current| index < current.1) {
group_enabled.insert(mutex_group.to_string(), (anchor, index, preset, mode));
}
}
ordered_enabled.extend(group_enabled.into_values());
ordered_enabled.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
ordered_enabled
.into_iter()
.map(|(_, _, preset, mode)| NormalizedPoolPreset { preset, mode })
.collect()
}
fn pool_preset_supported_for_provider(preset: &str, provider_type: &str) -> bool {
match preset {
"free_first" | "plus_first" | "pro_first" | "recent_refresh" | "team_first" => {
matches!(provider_type, "codex" | "kiro")
}
_ => true,
fn ai_pool_runtime_state(runtime: &AdminProviderPoolRuntimeState) -> AiPoolRuntimeState {
AiPoolRuntimeState {
sticky_bound_key_id: runtime.sticky_bound_key_id.clone(),
cooldown_reason_by_key: runtime.cooldown_reason_by_key.clone(),
cost_window_usage_by_key: runtime.cost_window_usage_by_key.clone(),
latency_avg_ms_by_key: runtime.latency_avg_ms_by_key.clone(),
lru_score_by_key: runtime.lru_score_by_key.clone(),
}
}
fn pool_preset_mutex_group(preset: &str) -> Option<&'static str> {
match preset {
"lru" | "cache_affinity" | "load_balance" | "single_account" => Some("distribution_mode"),
_ => None,
}
}
fn runtime_lru_score(runtime: &AdminProviderPoolRuntimeState, key_id: &str) -> Option<f64> {
runtime.lru_score_by_key.get(key_id).copied()
}
fn runtime_cost_usage(runtime: &AdminProviderPoolRuntimeState, key_id: &str) -> u64 {
runtime
.cost_window_usage_by_key
.get(key_id)
.copied()
.unwrap_or_default()
fn apply_ai_pool_orchestration(
mut candidate: EligibleLocalExecutionCandidate,
orchestration: AiPoolCandidateOrchestration,
) -> EligibleLocalExecutionCandidate {
candidate.orchestration = LocalExecutionCandidateMetadata {
candidate_group_id: orchestration.candidate_group_id,
pool_key_index: orchestration.pool_key_index,
};
candidate
}
#[cfg(test)]
mod tests {
use super::{
apply_local_execution_pool_scheduler_with_runtime_map, build_pool_catalog_key_context,
normalize_enabled_pool_presets, PoolCatalogKeyContext,
PoolCatalogKeyContext,
};
use crate::ai_pipeline::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_pipeline::PlannerAppState;
use crate::ai_serving::planner::candidate_resolution::EligibleLocalExecutionCandidate;
use crate::ai_serving::PlannerAppState;
use crate::data::GatewayDataState;
use crate::handlers::shared::provider_pool::{
AdminProviderPoolRuntimeState, AdminProviderPoolSchedulingPreset,
};
use crate::handlers::shared::provider_pool::AdminProviderPoolRuntimeState;
use crate::orchestration::LocalExecutionCandidateMetadata;
use crate::AppState;
use aether_ai_serving::{normalize_enabled_ai_pool_presets, AiPoolSchedulingPreset};
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
use aether_provider_transport::snapshot::{
@@ -1723,24 +1078,24 @@ mod tests {
#[test]
fn normalizes_distribution_mutex_group_to_first_enabled_member() {
let presets = normalize_enabled_pool_presets(
let presets = normalize_enabled_ai_pool_presets(
&[
AdminProviderPoolSchedulingPreset {
AiPoolSchedulingPreset {
preset: "lru".to_string(),
enabled: false,
mode: None,
},
AdminProviderPoolSchedulingPreset {
AiPoolSchedulingPreset {
preset: "single_account".to_string(),
enabled: true,
mode: None,
},
AdminProviderPoolSchedulingPreset {
AiPoolSchedulingPreset {
preset: "cache_affinity".to_string(),
enabled: true,
mode: None,
},
AdminProviderPoolSchedulingPreset {
AiPoolSchedulingPreset {
preset: "priority_first".to_string(),
enabled: true,
mode: None,
@@ -1749,13 +1104,7 @@ mod tests {
"openai",
);
assert_eq!(
presets
.iter()
.map(|item| item.preset.as_str())
.collect::<Vec<_>>(),
vec!["single_account", "priority_first"]
);
assert_eq!(presets, ["single_account", "priority_first"]);
}
#[test]
@@ -1843,7 +1192,7 @@ mod tests {
provider_api_format: "openai:chat".to_string(),
orchestration: LocalExecutionCandidateMetadata::default(),
ranking: None,
transport: Arc::new(crate::ai_pipeline::GatewayProviderTransportSnapshot {
transport: Arc::new(crate::ai_serving::GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: provider_id.to_string(),
name: provider_id.to_string(),

View File

@@ -0,0 +1,218 @@
use std::collections::BTreeMap;
use aether_ai_serving::{
build_ai_execution_report_context,
insert_provider_stream_event_api_format as insert_ai_provider_stream_event_api_format,
provider_stream_event_api_format_for_provider_type as ai_provider_stream_event_api_format_for_provider_type,
AiExecutionReportContextParts, AiRequestOrigin,
};
use aether_scheduler_core::SchedulerRankingOutcome;
use serde_json::{Map, Value};
use crate::ai_serving::{request_origin_from_headers, ExecutionRuntimeAuthContext, RequestOrigin};
use crate::orchestration::ExecutionAttemptIdentity;
pub(crate) struct LocalExecutionReportContextParts<'a> {
pub(crate) auth_context: &'a ExecutionRuntimeAuthContext,
pub(crate) request_id: &'a str,
pub(crate) candidate_id: &'a str,
pub(crate) attempt_identity: ExecutionAttemptIdentity,
pub(crate) model: &'a str,
pub(crate) provider_name: &'a str,
pub(crate) provider_id: &'a str,
pub(crate) endpoint_id: &'a str,
pub(crate) key_id: &'a str,
pub(crate) key_name: Option<&'a str>,
pub(crate) model_id: Option<&'a str>,
pub(crate) global_model_id: Option<&'a str>,
pub(crate) global_model_name: Option<&'a str>,
pub(crate) provider_api_format: &'a str,
pub(crate) client_api_format: &'a str,
pub(crate) mapped_model: Option<&'a str>,
pub(crate) candidate_group_id: Option<&'a str>,
pub(crate) ranking: Option<&'a SchedulerRankingOutcome>,
pub(crate) upstream_url: Option<&'a str>,
pub(crate) header_rules: Option<&'a Value>,
pub(crate) body_rules: Option<&'a Value>,
pub(crate) provider_request_method: Option<Value>,
pub(crate) provider_request_headers: Option<&'a BTreeMap<String, String>>,
pub(crate) original_headers: &'a http::HeaderMap,
pub(crate) request_origin: Option<RequestOrigin>,
pub(crate) original_request_body_json: Option<&'a Value>,
pub(crate) original_request_body_base64: Option<&'a str>,
pub(crate) client_requested_stream: bool,
pub(crate) upstream_is_stream: bool,
pub(crate) has_envelope: bool,
pub(crate) needs_conversion: bool,
pub(crate) extra_fields: Map<String, Value>,
}
pub(crate) fn build_local_execution_report_context(
parts: LocalExecutionReportContextParts<'_>,
) -> Value {
let RequestOrigin {
client_ip,
user_agent,
} = parts
.request_origin
.unwrap_or_else(|| request_origin_from_headers(parts.original_headers));
let original_headers = crate::ai_serving::collect_control_headers(parts.original_headers);
let original_request_body = crate::ai_serving::build_report_context_original_request_echo(
parts.original_request_body_json,
parts.original_request_body_base64,
);
build_ai_execution_report_context(AiExecutionReportContextParts {
auth_context: parts.auth_context,
request_id: parts.request_id,
candidate_id: parts.candidate_id,
candidate_index: parts.attempt_identity.candidate_index,
retry_index: parts.attempt_identity.retry_index,
pool_key_index: parts.attempt_identity.pool_key_index,
model: parts.model,
provider_name: parts.provider_name,
provider_id: parts.provider_id,
endpoint_id: parts.endpoint_id,
key_id: parts.key_id,
key_name: parts.key_name,
model_id: parts.model_id,
global_model_id: parts.global_model_id,
global_model_name: parts.global_model_name,
provider_api_format: parts.provider_api_format,
client_api_format: parts.client_api_format,
mapped_model: parts.mapped_model,
candidate_group_id: parts.candidate_group_id,
ranking: parts.ranking,
upstream_url: parts.upstream_url,
header_rules: parts.header_rules,
body_rules: parts.body_rules,
provider_request_method: parts.provider_request_method,
provider_request_headers: parts.provider_request_headers,
original_headers: &original_headers,
original_request_body,
request_origin: AiRequestOrigin {
client_ip,
user_agent,
},
client_requested_stream: parts.client_requested_stream,
upstream_is_stream: parts.upstream_is_stream,
has_envelope: parts.has_envelope,
needs_conversion: parts.needs_conversion,
extra_fields: parts.extra_fields,
})
}
pub(crate) fn provider_stream_event_api_format_for_provider_type(
provider_type: &str,
) -> Option<&'static str> {
ai_provider_stream_event_api_format_for_provider_type(provider_type)
}
pub(crate) fn insert_provider_stream_event_api_format(
extra_fields: &mut Map<String, Value>,
provider_type: &str,
) {
insert_ai_provider_stream_event_api_format(extra_fields, provider_type);
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use super::{
build_local_execution_report_context, provider_stream_event_api_format_for_provider_type,
LocalExecutionReportContextParts,
};
use crate::ai_serving::ExecutionRuntimeAuthContext;
use crate::ai_serving::RequestOrigin;
use crate::orchestration::ExecutionAttemptIdentity;
#[test]
fn codex_provider_uses_openai_responses_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("codex"),
Some("openai:responses")
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("CODEX"),
Some("openai:responses")
);
}
#[test]
fn ordinary_providers_do_not_override_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("openai"),
None
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("anthropic"),
None
);
}
#[test]
fn local_execution_report_context_records_request_origin() {
let auth_context = ExecutionRuntimeAuthContext {
user_id: "user-1".to_string(),
api_key_id: "api-key-1".to_string(),
username: None,
api_key_name: None,
balance_remaining: None,
access_allowed: true,
api_key_is_standalone: false,
};
let original_headers = http::HeaderMap::new();
let provider_request_headers = BTreeMap::new();
let report_context =
build_local_execution_report_context(LocalExecutionReportContextParts {
auth_context: &auth_context,
request_id: "trace-1",
candidate_id: "candidate-1",
attempt_identity: ExecutionAttemptIdentity::new(0, 0),
model: "gpt-5",
provider_name: "OpenAI",
provider_id: "provider-1",
endpoint_id: "endpoint-1",
key_id: "key-1",
key_name: None,
model_id: None,
global_model_id: None,
global_model_name: None,
provider_api_format: "openai:chat",
client_api_format: "openai:chat",
mapped_model: None,
candidate_group_id: None,
ranking: None,
upstream_url: None,
header_rules: None,
body_rules: None,
provider_request_method: None,
provider_request_headers: Some(&provider_request_headers),
original_headers: &original_headers,
request_origin: Some(RequestOrigin {
client_ip: Some("203.0.113.8".to_string()),
user_agent: Some("Claude-Code/1.0".to_string()),
}),
original_request_body_json: Some(&json!({"model": "gpt-5"})),
original_request_body_base64: None,
client_requested_stream: false,
upstream_is_stream: false,
has_envelope: false,
needs_conversion: false,
extra_fields: Map::new(),
});
assert_eq!(
report_context["client_ip"],
Value::String("203.0.113.8".to_string())
);
assert_eq!(
report_context["user_agent"],
Value::String("Claude-Code/1.0".to_string())
);
}
}

View File

@@ -1,12 +1,10 @@
use super::specialized::is_openai_image_stream_request;
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::{
is_matching_stream_request as is_matching_stream_request_impl,
use crate::ai_serving::GatewayControlDecision;
use crate::ai_serving::{
is_matching_stream_http_request as is_matching_stream_http_request_impl,
resolve_execution_runtime_stream_plan_kind as resolve_execution_runtime_stream_plan_kind_impl,
resolve_execution_runtime_sync_plan_kind as resolve_execution_runtime_sync_plan_kind_impl,
supports_stream_scheduler_decision_kind as supports_stream_scheduler_decision_kind_impl,
supports_sync_scheduler_decision_kind as supports_sync_scheduler_decision_kind_impl,
OPENAI_IMAGE_STREAM_PLAN_KIND,
};
pub(crate) fn resolve_execution_runtime_stream_plan_kind(
@@ -41,10 +39,7 @@ pub(crate) fn is_matching_stream_request(
body_json: &serde_json::Value,
body_base64: Option<&str>,
) -> bool {
if plan_kind == OPENAI_IMAGE_STREAM_PLAN_KIND {
return is_openai_image_stream_request(parts, body_json, body_base64);
}
is_matching_stream_request_impl(plan_kind, parts.uri.path(), body_json)
is_matching_stream_http_request_impl(plan_kind, parts, body_json, body_base64)
}
pub(crate) fn supports_sync_scheduler_decision_kind(plan_kind: &str) -> bool {
@@ -65,7 +60,7 @@ mod tests {
resolve_execution_runtime_sync_plan_kind, supports_stream_scheduler_decision_kind,
supports_sync_scheduler_decision_kind,
};
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_serving::GatewayControlDecision;
fn sample_decision(route_family: &str, route_kind: &str) -> GatewayControlDecision {
GatewayControlDecision {
@@ -83,7 +78,7 @@ mod tests {
}
#[test]
fn resolves_openai_chat_plan_kinds_via_pipeline_crate() {
fn resolves_openai_chat_plan_kinds_via_surface_crate() {
let request = Request::builder()
.method(Method::POST)
.uri("/v1/chat/completions")
@@ -103,7 +98,7 @@ mod tests {
}
#[test]
fn stream_matching_uses_pipeline_route_logic() {
fn stream_matching_uses_surface_route_logic() {
let request = Request::builder()
.method(Method::POST)
.uri("/v1/chat/completions")

View File

@@ -0,0 +1,256 @@
use aether_ai_serving::{
apply_ai_runtime_candidate_evaluation_progress,
apply_ai_runtime_candidate_evaluation_progress_preserving_candidate_signal,
apply_ai_runtime_candidate_evaluation_progress_to_diagnostic,
apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic,
apply_ai_runtime_candidate_terminal_reason, build_ai_runtime_candidate_evaluation_diagnostic,
build_ai_runtime_execution_exhausted_diagnostic, record_ai_runtime_candidate_skip_reason,
record_ai_runtime_candidate_skip_reason_on_diagnostic,
set_ai_runtime_candidate_evaluation_diagnostic, set_ai_runtime_execution_exhausted_diagnostic,
set_ai_runtime_miss_diagnostic_reason, AiRuntimeMissDiagnosticFields,
AiRuntimeMissDiagnosticPort,
};
use crate::ai_serving::GatewayControlDecision;
use crate::{AppState, LocalExecutionRuntimeMissDiagnostic};
struct GatewayRuntimeMissDiagnosticPort<'a> {
state: Option<&'a AppState>,
}
impl AiRuntimeMissDiagnosticFields for LocalExecutionRuntimeMissDiagnostic {
fn set_reason(&mut self, reason: String) {
self.reason = reason;
}
fn set_candidate_count(&mut self, candidate_count: usize) {
self.candidate_count = Some(candidate_count);
}
fn candidate_count(&self) -> Option<usize> {
self.candidate_count
}
fn skipped_candidate_count(&self) -> Option<usize> {
self.skipped_candidate_count
}
fn skip_reason_count(&self, skip_reason: &str) -> usize {
self.skip_reasons.get(skip_reason).copied().unwrap_or(0)
}
fn skip_reason_len(&self) -> usize {
self.skip_reasons.len()
}
fn record_skip_reason(&mut self, skip_reason: &'static str) {
*self
.skip_reasons
.entry(skip_reason.to_string())
.or_insert(0) += 1;
*self.skipped_candidate_count.get_or_insert(0) += 1;
}
}
impl AiRuntimeMissDiagnosticPort for GatewayRuntimeMissDiagnosticPort<'_> {
type Decision = GatewayControlDecision;
type Diagnostic = LocalExecutionRuntimeMissDiagnostic;
fn build_runtime_miss_diagnostic(
&self,
decision: &Self::Decision,
plan_kind: &str,
requested_model: Option<&str>,
reason: &str,
) -> Self::Diagnostic {
LocalExecutionRuntimeMissDiagnostic {
reason: reason.to_string(),
route_family: decision.route_family.clone(),
route_kind: decision.route_kind.clone(),
public_path: Some(decision.public_path.clone()),
plan_kind: Some(plan_kind.to_string()),
requested_model: requested_model.map(ToOwned::to_owned),
candidate_count: None,
skipped_candidate_count: None,
skip_reasons: std::collections::BTreeMap::new(),
}
}
fn set_candidate_count(&self, diagnostic: &mut Self::Diagnostic, candidate_count: usize) {
AiRuntimeMissDiagnosticFields::set_candidate_count(diagnostic, candidate_count);
}
fn apply_candidate_evaluation_progress(
&self,
diagnostic: &mut Self::Diagnostic,
candidate_count: usize,
) {
apply_ai_runtime_candidate_evaluation_progress_to_diagnostic(diagnostic, candidate_count);
}
fn apply_candidate_terminal_plan_reason(
&self,
diagnostic: &mut Self::Diagnostic,
no_plan_reason: &'static str,
) {
apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic(diagnostic, no_plan_reason);
}
fn record_candidate_skip_reason(
&self,
diagnostic: &mut Self::Diagnostic,
skip_reason: &'static str,
) {
record_ai_runtime_candidate_skip_reason_on_diagnostic(diagnostic, skip_reason);
}
fn set_runtime_miss_diagnostic(&self, trace_id: &str, diagnostic: Self::Diagnostic) {
self.state
.expect("runtime miss diagnostic setter requires gateway state")
.set_local_execution_runtime_miss_diagnostic(trace_id, diagnostic);
}
fn mutate_runtime_miss_diagnostic<F>(&self, trace_id: &str, apply: F)
where
F: FnOnce(&mut Self::Diagnostic) + Send,
{
self.state
.expect("runtime miss diagnostic mutator requires gateway state")
.mutate_local_execution_runtime_miss_diagnostic(trace_id, apply);
}
fn runtime_miss_diagnostic_has_candidate_signal(&self, trace_id: &str) -> bool {
self.state
.expect("runtime miss diagnostic signal check requires gateway state")
.local_execution_runtime_miss_diagnostic_has_candidate_signal(trace_id)
}
}
pub(crate) fn set_local_runtime_miss_diagnostic_reason(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
reason: &str,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
set_ai_runtime_miss_diagnostic_reason(
&port,
trace_id,
decision,
plan_kind,
requested_model,
reason,
);
}
pub(crate) fn build_local_runtime_execution_exhausted_diagnostic(
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) -> LocalExecutionRuntimeMissDiagnostic {
let port = GatewayRuntimeMissDiagnosticPort { state: None };
build_ai_runtime_execution_exhausted_diagnostic(
&port,
decision,
plan_kind,
requested_model,
candidate_count,
)
}
pub(crate) fn set_local_runtime_execution_exhausted_diagnostic(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
set_ai_runtime_execution_exhausted_diagnostic(
&port,
trace_id,
decision,
plan_kind,
requested_model,
candidate_count,
);
}
pub(crate) fn build_local_runtime_candidate_evaluation_diagnostic(
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) -> LocalExecutionRuntimeMissDiagnostic {
let port = GatewayRuntimeMissDiagnosticPort { state: None };
build_ai_runtime_candidate_evaluation_diagnostic(
&port,
decision,
plan_kind,
requested_model,
candidate_count,
)
}
pub(crate) fn set_local_runtime_candidate_evaluation_diagnostic(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
requested_model: Option<&str>,
candidate_count: usize,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
set_ai_runtime_candidate_evaluation_diagnostic(
&port,
trace_id,
decision,
plan_kind,
requested_model,
candidate_count,
);
}
pub(crate) fn apply_local_runtime_candidate_evaluation_progress(
state: &AppState,
trace_id: &str,
candidate_count: usize,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
apply_ai_runtime_candidate_evaluation_progress(&port, trace_id, candidate_count);
}
pub(crate) fn apply_local_runtime_candidate_evaluation_progress_preserving_candidate_signal(
state: &AppState,
trace_id: &str,
candidate_count: usize,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
apply_ai_runtime_candidate_evaluation_progress_preserving_candidate_signal(
&port,
trace_id,
candidate_count,
);
}
pub(crate) fn apply_local_runtime_candidate_terminal_reason(
state: &AppState,
trace_id: &str,
no_plan_reason: &'static str,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
apply_ai_runtime_candidate_terminal_reason(&port, trace_id, no_plan_reason);
}
pub(crate) fn record_local_runtime_candidate_skip_reason(
state: &AppState,
trace_id: &str,
skip_reason: &'static str,
) {
let port = GatewayRuntimeMissDiagnosticPort { state: Some(state) };
record_ai_runtime_candidate_skip_reason(&port, trace_id, skip_reason);
}

View File

@@ -0,0 +1,53 @@
use crate::ai_serving::planner::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_standard_stream_plan_from_decision, build_standard_sync_plan_from_decision,
AiStreamAttempt, AiSyncAttempt,
};
use crate::ai_serving::AiExecutionDecision;
use crate::GatewayError;
pub(crate) use aether_ai_serving::{
ai_gemini_files_spec_metadata as local_gemini_files_spec_metadata,
ai_openai_image_spec_metadata as local_openai_image_spec_metadata,
ai_openai_responses_spec_metadata as local_openai_responses_spec_metadata,
ai_requested_model_family_for_same_format_provider as requested_model_family_for_same_format_provider,
ai_requested_model_family_for_standard_source as requested_model_family_for_standard_source,
ai_requested_model_family_for_video_create as requested_model_family_for_video_create,
ai_same_format_provider_spec_metadata as local_same_format_provider_spec_metadata,
ai_standard_spec_metadata as local_standard_spec_metadata,
ai_video_create_spec_metadata as local_video_create_spec_metadata,
AiExecutionSurfaceSpecMetadata as LocalExecutionSurfaceSpecMetadata,
AiRequestedModelFamily as RequestedModelFamily,
};
pub(crate) fn build_sync_plan_from_requested_model_family(
family: RequestedModelFamily,
parts: &http::request::Parts,
body_json: &serde_json::Value,
payload: AiExecutionDecision,
) -> Result<Option<AiSyncAttempt>, GatewayError> {
match family {
RequestedModelFamily::Standard => {
build_standard_sync_plan_from_decision(parts, body_json, payload)
}
RequestedModelFamily::Gemini => {
build_gemini_sync_plan_from_decision(parts, body_json, payload)
}
}
}
pub(crate) fn build_stream_plan_from_requested_model_family(
family: RequestedModelFamily,
parts: &http::request::Parts,
body_json: &serde_json::Value,
payload: AiExecutionDecision,
) -> Result<Option<AiStreamAttempt>, GatewayError> {
match family {
RequestedModelFamily::Standard => {
build_standard_stream_plan_from_decision(parts, body_json, payload, false)
}
RequestedModelFamily::Gemini => {
build_gemini_stream_plan_from_decision(parts, body_json, payload)
}
}
}

View File

@@ -4,17 +4,17 @@ mod support;
use tracing::warn;
use crate::ai_pipeline::planner::plan_builders::{
use crate::ai_serving::planner::plan_builders::{
build_passthrough_stream_plan_from_decision, build_passthrough_sync_plan_from_decision,
LocalStreamPlanAndReport, LocalSyncPlanAndReport,
AiStreamAttempt, AiSyncAttempt,
};
use crate::ai_pipeline::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_pipeline::GatewayControlDecision;
use crate::ai_pipeline::{
use crate::ai_serving::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_serving::GatewayControlDecision;
use crate::ai_serving::{
resolve_gemini_files_stream_spec as resolve_stream_spec,
resolve_gemini_files_sync_spec as resolve_sync_spec, LocalGeminiFilesSpec,
};
use crate::{AppState, GatewayControlSyncDecisionResponse, GatewayError};
use crate::{AiExecutionDecision, AppState, GatewayError};
use self::decision::maybe_build_local_gemini_files_decision_payload_for_candidate;
use self::support::{
@@ -30,7 +30,7 @@ pub(crate) async fn build_local_gemini_files_sync_plan_and_reports_for_kind(
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Vec<LocalSyncPlanAndReport>, GatewayError> {
) -> Result<Vec<AiSyncAttempt>, GatewayError> {
let Some(spec) = resolve_sync_spec(plan_kind) else {
return Ok(Vec::new());
};
@@ -54,7 +54,7 @@ pub(crate) async fn build_local_gemini_files_stream_plan_and_reports_for_kind(
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Vec<LocalStreamPlanAndReport>, GatewayError> {
) -> Result<Vec<AiStreamAttempt>, GatewayError> {
let Some(spec) = resolve_stream_spec(plan_kind) else {
return Ok(Vec::new());
};
@@ -71,7 +71,7 @@ pub(crate) async fn maybe_build_sync_local_gemini_files_decision_payload(
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(spec) = resolve_sync_spec(plan_kind) else {
return Ok(None);
};
@@ -111,7 +111,7 @@ pub(crate) async fn maybe_build_stream_local_gemini_files_decision_payload(
trace_id: &str,
decision: &GatewayControlDecision,
plan_kind: &str,
) -> Result<Option<GatewayControlSyncDecisionResponse>, GatewayError> {
) -> Result<Option<AiExecutionDecision>, GatewayError> {
let Some(spec) = resolve_stream_spec(plan_kind) else {
return Ok(None);
};
@@ -155,7 +155,7 @@ async fn build_local_sync_plan_and_reports(
trace_id: &str,
decision: &GatewayControlDecision,
spec: LocalGeminiFilesSpec,
) -> Result<Vec<LocalSyncPlanAndReport>, GatewayError> {
) -> Result<Vec<AiSyncAttempt>, GatewayError> {
let spec_metadata = local_gemini_files_spec_metadata(spec);
let Some(input) = resolve_local_gemini_files_decision_input(state, trace_id, decision).await
else {
@@ -206,7 +206,7 @@ async fn build_local_stream_plan_and_reports(
trace_id: &str,
decision: &GatewayControlDecision,
spec: LocalGeminiFilesSpec,
) -> Result<Vec<LocalStreamPlanAndReport>, GatewayError> {
) -> Result<Vec<AiStreamAttempt>, GatewayError> {
let spec_metadata = local_gemini_files_spec_metadata(spec);
let Some(input) = resolve_local_gemini_files_decision_input(state, trace_id, decision).await
else {

View File

@@ -1,18 +1,18 @@
use serde_json::json;
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
use crate::ai_pipeline::planner::payload_metadata::{
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
};
use crate::ai_pipeline::planner::report_context::{
use crate::ai_serving::build_request_trace_proxy_value;
use crate::ai_serving::planner::report_context::{
build_local_execution_report_context, LocalExecutionReportContextParts,
};
use crate::ai_pipeline::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_pipeline::transport::{
use crate::ai_serving::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_serving::planner::{
build_ai_execution_decision_response, AiExecutionDecisionResponseParts,
};
use crate::ai_serving::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
};
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, PlannerAppState};
use crate::{AppState, GatewayControlSyncDecisionResponse};
use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState};
use crate::{AiExecutionDecision, AppState};
use super::request::resolve_local_gemini_files_candidate_payload_parts;
use super::support::{
@@ -31,7 +31,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
input: &LocalGeminiFilesDecisionInput,
attempt: LocalGeminiFilesCandidateAttempt,
spec: LocalGeminiFilesSpec,
) -> Option<GatewayControlSyncDecisionResponse> {
) -> Option<AiExecutionDecision> {
let spec_metadata = local_gemini_files_spec_metadata(spec);
let planner_state = PlannerAppState::new(state);
let attempt_identity = attempt.attempt_identity();
@@ -54,6 +54,10 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
} = attempt;
let candidate = eligible.candidate;
let transport = resolved.transport;
let (execution_strategy, conversion_mode) = ai_local_execution_contract_for_formats(
GEMINI_FILES_CLIENT_API_FORMAT,
GEMINI_FILES_CLIENT_API_FORMAT,
);
let proxy = planner_state
.app()
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
@@ -90,7 +94,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
provider_request_method: None,
provider_request_headers: None,
original_headers: &parts.headers,
request_origin: Some(crate::ai_pipeline::request_origin_from_parts(parts)),
request_origin: Some(crate::ai_serving::request_origin_from_parts(parts)),
original_request_body_json: Some(body_json),
original_request_body_base64: resolved.provider_request_body_base64.as_deref(),
client_requested_stream: spec_metadata.require_streaming,
@@ -110,12 +114,12 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
file_name: _,
} = resolved;
Some(build_local_execution_decision_response(
LocalExecutionDecisionResponseParts {
Some(build_ai_execution_decision_response(
AiExecutionDecisionResponseParts {
decision_is_stream: spec_metadata.require_streaming,
decision_kind: spec_metadata.decision_kind.to_string(),
execution_strategy: ExecutionStrategy::LocalSameFormat,
conversion_mode: ConversionMode::None,
execution_strategy,
conversion_mode,
request_id: trace_id.to_string(),
candidate_id: candidate_id.clone(),
provider_name: transport.provider.name.clone(),

View File

@@ -1,17 +1,14 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use serde_json::json;
use crate::ai_pipeline::contracts::GEMINI_FILES_UPLOAD_PLAN_KIND;
use crate::ai_pipeline::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_pipeline::transport::auth::{
build_passthrough_headers_with_auth, resolve_local_gemini_auth,
use crate::ai_serving::planner::spec_metadata::local_gemini_files_spec_metadata;
use crate::ai_serving::transport::{
build_gemini_files_headers, build_gemini_files_request_body, build_gemini_files_upstream_url,
gemini_files_transport_unsupported_reason, resolve_gemini_files_auth, GeminiFilesHeadersInput,
GeminiFilesRequestBodyError,
};
use crate::ai_pipeline::transport::local_gemini_transport_unsupported_reason_with_network;
use crate::ai_pipeline::transport::url::build_gemini_files_passthrough_url;
use crate::ai_pipeline::transport::{apply_local_body_rules, apply_local_header_rules};
use crate::ai_pipeline::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::ai_serving::GEMINI_FILES_UPLOAD_PLAN_KIND;
use crate::ai_serving::{CandidateFailureDiagnostic, GatewayProviderTransportSnapshot};
use crate::AppState;
use super::support::{
@@ -49,10 +46,9 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
let candidate = &attempt.eligible.candidate;
let transport = &attempt.eligible.transport;
if let Some(skip_reason) = local_gemini_transport_unsupported_reason_with_network(
transport,
GEMINI_FILES_CANDIDATE_API_FORMAT,
) {
if let Some(skip_reason) =
gemini_files_transport_unsupported_reason(transport, GEMINI_FILES_CANDIDATE_API_FORMAT)
{
mark_skipped_local_gemini_files_candidate(
state,
input,
@@ -66,7 +62,7 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
return None;
}
let Some((auth_header, auth_value)) = resolve_local_gemini_auth(transport) else {
let Some((auth_header, auth_value)) = resolve_gemini_files_auth(transport) else {
mark_skipped_local_gemini_files_candidate(
state,
input,
@@ -80,18 +76,9 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
return None;
};
let custom_path = transport
.endpoint
.custom_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let passthrough_path = custom_path.unwrap_or(parts.uri.path());
let Some(upstream_url) = build_gemini_files_passthrough_url(
&transport.endpoint.base_url,
passthrough_path,
parts.uri.query(),
) else {
let Some(upstream_url) =
build_gemini_files_upstream_url(transport, parts.uri.path(), parts.uri.query())
else {
mark_skipped_local_gemini_files_candidate_with_failure_diagnostic(
state,
input,
@@ -110,46 +97,33 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
return None;
};
let mut provider_request_body = if spec_metadata.decision_kind == GEMINI_FILES_UPLOAD_PLAN_KIND
&& !body_is_empty
&& body_base64.is_none()
{
Some(body_json.clone())
} else {
None
};
let provider_request_body_base64 =
if spec_metadata.decision_kind == GEMINI_FILES_UPLOAD_PLAN_KIND {
body_base64
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
} else {
None
};
if provider_request_body_base64.is_some() && transport.endpoint.body_rules.is_some() {
mark_skipped_local_gemini_files_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_body_rules_unsupported_for_binary_upload",
CandidateFailureDiagnostic::body_rules_unsupported_for_binary_upload(
GEMINI_FILES_CLIENT_API_FORMAT,
GEMINI_FILES_CANDIDATE_API_FORMAT,
"gemini_files_binary_upload",
),
)
.await;
return None;
}
if let Some(body) = provider_request_body.as_mut() {
if !apply_local_body_rules(
body,
transport.endpoint.body_rules.as_ref(),
Some(body_json),
) {
let body_parts = match build_gemini_files_request_body(
body_json,
body_base64,
body_is_empty,
spec_metadata.decision_kind == GEMINI_FILES_UPLOAD_PLAN_KIND,
transport.endpoint.body_rules.as_ref(),
) {
Ok(parts) => parts,
Err(GeminiFilesRequestBodyError::BodyRulesUnsupportedForBinaryUpload) => {
mark_skipped_local_gemini_files_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"transport_body_rules_unsupported_for_binary_upload",
CandidateFailureDiagnostic::body_rules_unsupported_for_binary_upload(
GEMINI_FILES_CLIENT_API_FORMAT,
GEMINI_FILES_CANDIDATE_API_FORMAT,
"gemini_files_binary_upload",
),
)
.await;
return None;
}
Err(GeminiFilesRequestBodyError::BodyRulesApplyFailed) => {
mark_skipped_local_gemini_files_candidate_with_failure_diagnostic(
state,
input,
@@ -167,31 +141,18 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
.await;
return None;
}
}
};
let mut provider_request_headers = build_passthrough_headers_with_auth(
&parts.headers,
&auth_header,
&auth_value,
&BTreeMap::new(),
);
let null_original_request_body = serde_json::Value::Null;
let base64_original_request_body = provider_request_body_base64
.as_ref()
.map(|body_bytes_b64| json!({ "body_bytes_b64": body_bytes_b64 }));
let original_request_body = base64_original_request_body
.as_ref()
.or_else(|| (!body_is_empty).then_some(body_json))
.unwrap_or(&null_original_request_body);
if !apply_local_header_rules(
&mut provider_request_headers,
transport.endpoint.header_rules.as_ref(),
&[&auth_header, "content-type"],
provider_request_body
.as_ref()
.unwrap_or(original_request_body),
Some(original_request_body),
) {
let Some(provider_request_headers) = build_gemini_files_headers(GeminiFilesHeadersInput {
headers: &parts.headers,
auth_header: &auth_header,
auth_value: &auth_value,
header_rules: transport.endpoint.header_rules.as_ref(),
provider_request_body: body_parts.provider_request_body.as_ref(),
provider_request_body_base64: body_parts.provider_request_body_base64.as_deref(),
original_request_body_json: body_json,
original_body_is_empty: body_is_empty,
}) else {
mark_skipped_local_gemini_files_candidate_with_failure_diagnostic(
state,
input,
@@ -208,7 +169,7 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
)
.await;
return None;
}
};
let file_name = parts
.uri
@@ -222,8 +183,8 @@ pub(super) async fn resolve_local_gemini_files_candidate_payload_parts(
auth_header,
auth_value,
provider_request_headers,
provider_request_body,
provider_request_body_base64,
provider_request_body: body_parts.provider_request_body,
provider_request_body_base64: body_parts.provider_request_body_base64,
upstream_url,
file_name,
})

Some files were not shown because too many files have changed in this diff Show More