feat: improve failover rules and request timeline

This commit is contained in:
elky
2026-06-11 00:49:29 +08:00
parent 31fade82f6
commit 30b545785f
15 changed files with 1576 additions and 199 deletions
@@ -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]