mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
feat(gateway): harden provider request execution
Preserve exact request payloads and model client surface and API operation explicitly. Add Anthropic compatibility profiles, bounded stream commitment, and scoped OAuth retry behavior across provider transports.
This commit is contained in:
@@ -18,6 +18,7 @@ use base64::Engine as _;
|
||||
use serde_json::{json, Map, Value};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ai_serving::{build_core_error_body_for_client_format, LocalCoreSyncErrorKind};
|
||||
use crate::constants::{
|
||||
EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS, LOCAL_EXECUTION_RUNTIME_MISS_REASON_HEADER,
|
||||
};
|
||||
@@ -32,6 +33,9 @@ pub(crate) enum LocalExecutionRequestOutcome {
|
||||
NoPath,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct DeferredUpstreamResponse;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LocalExecutionExhaustion {
|
||||
request_id: String,
|
||||
@@ -70,6 +74,18 @@ impl LocalExecutionRequestOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_deferred_upstream_response(mut response: Response<Body>) -> Response<Body> {
|
||||
response.extensions_mut().insert(DeferredUpstreamResponse);
|
||||
response
|
||||
}
|
||||
|
||||
pub(crate) fn is_deferred_upstream_response(response: &Response<Body>) -> bool {
|
||||
response
|
||||
.extensions()
|
||||
.get::<DeferredUpstreamResponse>()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
impl LocalExecutionRuntimeMissContext {
|
||||
pub(crate) fn persisted_candidate_count(&self) -> usize {
|
||||
self.candidate_contexts.len()
|
||||
@@ -316,12 +332,12 @@ pub(crate) async fn record_failed_usage_for_exhausted_request(
|
||||
);
|
||||
}
|
||||
data.client_response_headers = Some(Value::Object(client_headers));
|
||||
data.client_response_body = Some(json!({
|
||||
"error": {
|
||||
"type": "http_error",
|
||||
"message": beautify_local_execution_client_error_message(local_execution_runtime_miss_detail),
|
||||
}
|
||||
}));
|
||||
let client_message =
|
||||
beautify_local_execution_client_error_message(local_execution_runtime_miss_detail);
|
||||
data.client_response_body = Some(runtime_miss_client_error_body(
|
||||
data.api_format.as_deref(),
|
||||
&client_message,
|
||||
));
|
||||
|
||||
let mut request_metadata = match data.request_metadata.take() {
|
||||
Some(Value::Object(object)) => object,
|
||||
@@ -392,12 +408,7 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
||||
let status_code = http::StatusCode::SERVICE_UNAVAILABLE.as_u16();
|
||||
let client_message =
|
||||
beautify_local_execution_client_error_message(local_execution_runtime_miss_detail);
|
||||
let client_body = json!({
|
||||
"error": {
|
||||
"type": "http_error",
|
||||
"message": client_message,
|
||||
}
|
||||
});
|
||||
let client_body = runtime_miss_client_error_body(api_format.as_deref(), &client_message);
|
||||
let mut client_headers = Map::from_iter([(
|
||||
"content-type".to_string(),
|
||||
Value::String("application/json".to_string()),
|
||||
@@ -654,6 +665,30 @@ fn json_header_map() -> Value {
|
||||
)]))
|
||||
}
|
||||
|
||||
fn runtime_miss_client_error_body(api_format: Option<&str>, message: &str) -> Value {
|
||||
let fallback = json!({
|
||||
"error": {
|
||||
"type": "http_error",
|
||||
"message": message,
|
||||
}
|
||||
});
|
||||
let is_claude = api_format.is_some_and(|format| {
|
||||
crate::ai_serving::normalize_api_format_alias(format)
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
});
|
||||
if !is_claude {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
build_core_error_body_for_client_format(
|
||||
"claude:messages",
|
||||
message,
|
||||
None,
|
||||
LocalCoreSyncErrorKind::Overloaded,
|
||||
)
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
fn runtime_miss_original_headers_json(headers: &HeaderMap) -> Value {
|
||||
let mut headers = crate::headers::collect_control_headers(headers);
|
||||
for (name, value) in headers.iter_mut() {
|
||||
@@ -1135,7 +1170,7 @@ fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
||||
mod tests {
|
||||
use super::{
|
||||
apply_runtime_miss_usage_routing, beautify_local_execution_client_error_message,
|
||||
request_candidate_represents_provider_execution,
|
||||
request_candidate_represents_provider_execution, runtime_miss_client_error_body,
|
||||
select_last_runtime_miss_executed_candidate, LocalExecutionRuntimeMissContext,
|
||||
RuntimeMissCandidateContext,
|
||||
};
|
||||
@@ -1169,6 +1204,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_miss_usage_body_matches_claude_client_envelope() {
|
||||
let claude = runtime_miss_client_error_body(Some("claude:messages"), "busy");
|
||||
assert_eq!(claude["type"], "error");
|
||||
assert_eq!(claude["error"]["type"], "overloaded_error");
|
||||
|
||||
let openai = runtime_miss_client_error_body(Some("openai:chat"), "busy");
|
||||
assert_eq!(openai["error"]["type"], "http_error");
|
||||
assert!(openai.get("type").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_miss_routing_moves_to_typed_usage_fields_and_keeps_metadata_lightweight() {
|
||||
let mut data = UsageEventData::default();
|
||||
|
||||
Reference in New Issue
Block a user