mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat: improve failover rules and request timeline
This commit is contained in:
@@ -19,6 +19,93 @@ pub fn decode_json_data_line(line: &[u8]) -> Option<Value> {
|
||||
serde_json::from_str(data_line).ok()
|
||||
}
|
||||
|
||||
pub fn unsupported_stream_event_message(payload: &Value) -> String {
|
||||
const BASE_MESSAGE: &str = "Unsupported provider stream event cannot be converted losslessly";
|
||||
match unsupported_stream_event_diagnostic(payload) {
|
||||
Some(diagnostic) if !diagnostic.is_empty() => format!("{BASE_MESSAGE}: {diagnostic}"),
|
||||
_ => BASE_MESSAGE.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_stream_event_diagnostic(payload: &Value) -> Option<String> {
|
||||
let mut details = Vec::new();
|
||||
if let Some((path, value)) = unsupported_stream_event_primary_field(payload) {
|
||||
details.push(format!("field {path} = {value}"));
|
||||
} else if let Some(path) = unsupported_stream_event_single_field(payload) {
|
||||
details.push(format!("field {path} is unsupported"));
|
||||
}
|
||||
|
||||
if let Some(fields) = unsupported_stream_event_field_list(payload) {
|
||||
details.push(format!("fields: {fields}"));
|
||||
}
|
||||
|
||||
if details.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(details.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_stream_event_primary_field(payload: &Value) -> Option<(&'static str, String)> {
|
||||
const STRING_FIELD_PATHS: &[(&str, &str)] = &[
|
||||
("$.item.type", "/item/type"),
|
||||
("$.content_block.type", "/content_block/type"),
|
||||
("$.delta.type", "/delta/type"),
|
||||
("$.part.type", "/part/type"),
|
||||
("$.payload.type", "/payload/type"),
|
||||
("$.type", "/type"),
|
||||
("$.event", "/event"),
|
||||
];
|
||||
|
||||
STRING_FIELD_PATHS
|
||||
.iter()
|
||||
.find_map(|(display_path, pointer)| {
|
||||
payload
|
||||
.pointer(pointer)
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|value| (*display_path, json!(value.trim()).to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn unsupported_stream_event_single_field(payload: &Value) -> Option<String> {
|
||||
let object = payload.as_object()?;
|
||||
if object.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
object.keys().next().map(|key| json_path_key(key))
|
||||
}
|
||||
|
||||
fn unsupported_stream_event_field_list(payload: &Value) -> Option<String> {
|
||||
let object = payload.as_object()?;
|
||||
if object.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let fields = object
|
||||
.keys()
|
||||
.take(8)
|
||||
.map(|key| key.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
if object.len() > 8 {
|
||||
Some(format!("{fields}, ..."))
|
||||
} else {
|
||||
Some(fields)
|
||||
}
|
||||
}
|
||||
|
||||
fn json_path_key(key: &str) -> String {
|
||||
if !key.is_empty()
|
||||
&& key.chars().enumerate().all(|(index, ch)| {
|
||||
ch == '_' || ch.is_ascii_alphabetic() || (index > 0 && ch.is_ascii_digit())
|
||||
})
|
||||
{
|
||||
format!("$.{key}")
|
||||
} else {
|
||||
format!("$[{}]", json!(key))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_identity(
|
||||
response_id: Option<&str>,
|
||||
model: Option<&str>,
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::formats::shared::error_body::{
|
||||
use crate::formats::shared::sse::encode_json_sse;
|
||||
use crate::formats::shared::stream_core::common::{
|
||||
decode_json_data_line, openai_stream_terminal_error_body, openai_stream_terminal_error_message,
|
||||
CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
|
||||
unsupported_stream_event_message, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
|
||||
};
|
||||
use crate::formats::shared::AiSurfaceFinalizeError;
|
||||
|
||||
@@ -95,9 +95,9 @@ impl StreamingStandardFormatMatrix {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if matches!(&frame.event, CanonicalStreamEvent::UnknownEvent(_)) {
|
||||
if let CanonicalStreamEvent::UnknownEvent(payload) = &frame.event {
|
||||
self.terminated = true;
|
||||
out.extend(client.emit_unknown_event()?);
|
||||
out.extend(client.emit_unknown_event(payload)?);
|
||||
break;
|
||||
}
|
||||
out.extend(client.emit(frame)?);
|
||||
@@ -403,10 +403,10 @@ impl ClientStreamEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_unknown_event(&mut self) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
|
||||
fn emit_unknown_event(&mut self, payload: &Value) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
|
||||
let Some(error_body) = build_core_error_body_for_client_format(
|
||||
self.api_format(),
|
||||
"Unsupported provider stream event cannot be converted losslessly",
|
||||
&unsupported_stream_event_message(payload),
|
||||
Some("unsupported_stream_event"),
|
||||
LocalCoreSyncErrorKind::ServerError,
|
||||
) else {
|
||||
@@ -422,7 +422,8 @@ impl ClientStreamEmitter {
|
||||
let Some(error_body) = build_core_error_body_for_client_format(
|
||||
self.api_format(),
|
||||
&format!(
|
||||
"Unsupported provider stream finish reason {finish_reason:?} cannot be converted losslessly"
|
||||
"Unsupported provider stream finish reason cannot be converted losslessly: field $.finish_reason = {}",
|
||||
serde_json::json!(finish_reason)
|
||||
),
|
||||
Some("unsupported_finish_reason"),
|
||||
LocalCoreSyncErrorKind::ServerError,
|
||||
@@ -962,6 +963,10 @@ mod tests {
|
||||
sse.contains("Unsupported provider stream event cannot be converted losslessly"),
|
||||
"{client_api_format}: {sse}"
|
||||
);
|
||||
assert!(
|
||||
sse.contains("field $.type = \\\"response.future.delta\\\""),
|
||||
"{sse}"
|
||||
);
|
||||
assert!(sse.contains(marker), "{client_api_format}: {sse}");
|
||||
assert!(matrix
|
||||
.finish(&report_context)
|
||||
@@ -1212,6 +1217,10 @@ mod tests {
|
||||
sse.contains("Unsupported provider stream finish reason"),
|
||||
"{client_api_format}: {sse}"
|
||||
);
|
||||
assert!(
|
||||
sse.contains("field $.finish_reason = \\\"future_reason\\\""),
|
||||
"{client_api_format}: {sse}"
|
||||
);
|
||||
assert!(sse.contains("future_reason"), "{client_api_format}: {sse}");
|
||||
assert!(sse.contains(marker), "{client_api_format}: {sse}");
|
||||
assert!(matrix
|
||||
@@ -1278,6 +1287,10 @@ mod tests {
|
||||
sse.contains("Unsupported provider stream finish reason"),
|
||||
"{client_api_format}: {sse}"
|
||||
);
|
||||
assert!(
|
||||
sse.contains("field $.finish_reason = \\\"OTHER\\\""),
|
||||
"{client_api_format}: {sse}"
|
||||
);
|
||||
assert!(sse.contains("OTHER"), "{client_api_format}: {sse}");
|
||||
assert!(sse.contains(marker), "{client_api_format}: {sse}");
|
||||
assert!(matrix
|
||||
|
||||
@@ -30,8 +30,9 @@ use crate::formats::shared::response::{
|
||||
};
|
||||
use crate::formats::shared::stream_core::common::{
|
||||
content_part_from_openai_image_generation_item, gemini_usage_metadata_from_usage,
|
||||
map_openai_finish_reason_to_gemini, parse_json_arguments_value, CanonicalContentPart,
|
||||
CanonicalStreamEvent, CanonicalStreamFrame, CanonicalUsage,
|
||||
map_openai_finish_reason_to_gemini, parse_json_arguments_value,
|
||||
unsupported_stream_event_message, CanonicalContentPart, CanonicalStreamEvent,
|
||||
CanonicalStreamFrame, CanonicalUsage,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -1583,11 +1584,11 @@ fn ensure_no_unknown_provider_stream_events(
|
||||
};
|
||||
for raw_line in text.lines() {
|
||||
let frames = push_line(raw_line.as_bytes().to_vec())?;
|
||||
if frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.event, CanonicalStreamEvent::UnknownEvent(_)))
|
||||
{
|
||||
return Err(unsupported_stream_event_finalize_error());
|
||||
if let Some(payload) = frames.iter().find_map(|frame| match &frame.event {
|
||||
CanonicalStreamEvent::UnknownEvent(payload) => Some(payload),
|
||||
_ => None,
|
||||
}) {
|
||||
return Err(unsupported_stream_event_finalize_error(payload));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -3046,8 +3047,8 @@ fn try_aggregate_gemini_stream_sync_response(
|
||||
content,
|
||||
));
|
||||
}
|
||||
CanonicalStreamEvent::UnknownEvent(_) => {
|
||||
return Err(unsupported_stream_event_finalize_error())
|
||||
CanonicalStreamEvent::UnknownEvent(payload) => {
|
||||
return Err(unsupported_stream_event_finalize_error(&payload))
|
||||
}
|
||||
CanonicalStreamEvent::ReasoningSummaryDone => {}
|
||||
CanonicalStreamEvent::Finish {
|
||||
@@ -3076,8 +3077,8 @@ fn try_aggregate_gemini_stream_sync_response(
|
||||
model_version = Some(Value::String(frame.model.clone()));
|
||||
}
|
||||
match frame.event {
|
||||
CanonicalStreamEvent::UnknownEvent(_) => {
|
||||
return Err(unsupported_stream_event_finalize_error())
|
||||
CanonicalStreamEvent::UnknownEvent(payload) => {
|
||||
return Err(unsupported_stream_event_finalize_error(&payload))
|
||||
}
|
||||
CanonicalStreamEvent::Finish {
|
||||
finish_reason: frame_finish_reason,
|
||||
@@ -3140,8 +3141,8 @@ fn try_aggregate_gemini_stream_sync_response(
|
||||
Ok(Some(Value::Object(response)))
|
||||
}
|
||||
|
||||
fn unsupported_stream_event_finalize_error() -> AiSurfaceFinalizeError {
|
||||
AiSurfaceFinalizeError::new("Unsupported provider stream event cannot be converted losslessly")
|
||||
fn unsupported_stream_event_finalize_error(payload: &Value) -> AiSurfaceFinalizeError {
|
||||
AiSurfaceFinalizeError::new(unsupported_stream_event_message(payload))
|
||||
}
|
||||
|
||||
fn append_gemini_text_part(parts: &mut Vec<Value>, text: String, thought: bool) {
|
||||
@@ -3608,6 +3609,9 @@ mod tests {
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Unsupported provider stream event cannot be converted losslessly"));
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("field $.futurePart is unsupported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5140,6 +5144,9 @@ mod tests {
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Unsupported provider stream event cannot be converted losslessly"));
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("field $.type = \"future_event\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5175,6 +5182,9 @@ mod tests {
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("Unsupported provider stream event cannot be converted losslessly"));
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("field $.type = \"response.future.delta\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user