mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
fix(usage): 简化本地执行错误提示
This commit is contained in:
@@ -16,9 +16,10 @@ pub(crate) use candidate_loop::{
|
||||
};
|
||||
pub(crate) use orchestration::*;
|
||||
pub(crate) use outcome::{
|
||||
build_local_execution_exhaustion, build_local_execution_runtime_miss_context,
|
||||
record_failed_usage_for_exhausted_request, record_failed_usage_for_runtime_miss_request,
|
||||
LocalExecutionExhaustion, LocalExecutionRequestOutcome, LocalExecutionRuntimeMissContext,
|
||||
beautify_local_execution_client_error_message, build_local_execution_exhaustion,
|
||||
build_local_execution_runtime_miss_context, record_failed_usage_for_exhausted_request,
|
||||
record_failed_usage_for_runtime_miss_request, LocalExecutionExhaustion,
|
||||
LocalExecutionRequestOutcome, LocalExecutionRuntimeMissContext,
|
||||
};
|
||||
pub(crate) use plan_fallback::{
|
||||
maybe_execute_stream_via_plan_fallback, maybe_execute_sync_via_plan_fallback,
|
||||
|
||||
@@ -252,7 +252,7 @@ pub(crate) async fn record_failed_usage_for_exhausted_request(
|
||||
data.client_response_body = Some(json!({
|
||||
"error": {
|
||||
"type": "http_error",
|
||||
"message": local_execution_runtime_miss_detail,
|
||||
"message": beautify_local_execution_client_error_message(local_execution_runtime_miss_detail),
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -322,10 +322,12 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
||||
.filter(|value| !value.eq_ignore_ascii_case(model.as_str()));
|
||||
|
||||
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": local_execution_runtime_miss_detail,
|
||||
"message": client_message,
|
||||
}
|
||||
});
|
||||
let mut client_headers = Map::from_iter([(
|
||||
@@ -418,6 +420,66 @@ pub(crate) async fn record_failed_usage_for_runtime_miss_request(
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) fn beautify_local_execution_client_error_message(message: &str) -> String {
|
||||
let without_reason_code = strip_parenthesized_reason_code(message);
|
||||
let mut simplified = collapse_whitespace(without_reason_code.as_str());
|
||||
for marker in [
|
||||
"。请检查",
|
||||
"。请确认",
|
||||
". 请检查",
|
||||
". 请确认",
|
||||
"! 请检查",
|
||||
"! 请确认",
|
||||
"? 请检查",
|
||||
"? 请确认",
|
||||
"。Reason",
|
||||
". Reason",
|
||||
"。Code",
|
||||
". Code",
|
||||
] {
|
||||
if let Some(index) = simplified.find(marker) {
|
||||
simplified.truncate(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
trim_trailing_message_punctuation(simplified.as_str()).to_string()
|
||||
}
|
||||
|
||||
fn strip_parenthesized_reason_code(message: &str) -> String {
|
||||
let Some(reason_index) = message.find("原因代码") else {
|
||||
return message.to_string();
|
||||
};
|
||||
let Some((start, open)) = message[..reason_index]
|
||||
.char_indices()
|
||||
.rev()
|
||||
.find(|(_, ch)| *ch == '(' || *ch == '(')
|
||||
else {
|
||||
return message.to_string();
|
||||
};
|
||||
let close = if open == '(' { ')' } else { ')' };
|
||||
let Some(close_offset) = message[start..].find(close) else {
|
||||
return message[..start].to_string();
|
||||
};
|
||||
let end = start + close_offset + close.len_utf8();
|
||||
format!("{}{}", &message[..start], &message[end..])
|
||||
}
|
||||
|
||||
fn collapse_whitespace(message: &str) -> String {
|
||||
message.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn trim_trailing_message_punctuation(message: &str) -> &str {
|
||||
message
|
||||
.trim_end_matches(|ch: char| {
|
||||
ch.is_whitespace()
|
||||
|| matches!(
|
||||
ch,
|
||||
'。' | '.' | '!' | '?' | ';' | ';' | ',' | ',' | ':' | ':'
|
||||
)
|
||||
})
|
||||
.trim()
|
||||
}
|
||||
|
||||
fn select_last_failed_request_candidate(
|
||||
candidates: &[StoredRequestCandidate],
|
||||
) -> Option<&StoredRequestCandidate> {
|
||||
@@ -870,7 +932,8 @@ fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
apply_runtime_miss_usage_routing, request_candidate_represents_provider_execution,
|
||||
apply_runtime_miss_usage_routing, beautify_local_execution_client_error_message,
|
||||
request_candidate_represents_provider_execution,
|
||||
select_last_runtime_miss_executed_candidate, RuntimeMissCandidateContext,
|
||||
};
|
||||
use crate::constants::EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS;
|
||||
@@ -881,6 +944,22 @@ mod tests {
|
||||
use aether_usage_runtime::UsageEventData;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
#[test]
|
||||
fn local_execution_client_error_message_is_client_friendly() {
|
||||
assert_eq!(
|
||||
beautify_local_execution_client_error_message(
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)",
|
||||
),
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求"
|
||||
);
|
||||
assert_eq!(
|
||||
beautify_local_execution_client_error_message(
|
||||
"请求缺少 model 字段,无法选择上游提供商(openai/chat,原因代码: missing_requested_model)",
|
||||
),
|
||||
"请求缺少 model 字段,无法选择上游提供商"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_miss_routing_moves_to_typed_usage_fields_and_keeps_metadata_lightweight() {
|
||||
let mut data = UsageEventData::default();
|
||||
|
||||
@@ -36,9 +36,10 @@ use crate::control::{
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::executor::{
|
||||
build_local_execution_runtime_miss_context, maybe_execute_stream_request,
|
||||
maybe_execute_sync_request, record_failed_usage_for_exhausted_request,
|
||||
record_failed_usage_for_runtime_miss_request, LocalExecutionRequestOutcome,
|
||||
beautify_local_execution_client_error_message, build_local_execution_runtime_miss_context,
|
||||
maybe_execute_stream_request, maybe_execute_sync_request,
|
||||
record_failed_usage_for_exhausted_request, record_failed_usage_for_runtime_miss_request,
|
||||
LocalExecutionRequestOutcome,
|
||||
};
|
||||
use crate::frontdoor_loop_guard::{
|
||||
frontdoor_self_loop_public_ai_path, request_has_execution_runtime_loop_guard,
|
||||
@@ -1364,7 +1365,10 @@ pub(crate) async fn proxy_request(
|
||||
&trace_id,
|
||||
control_decision,
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
local_execution_runtime_miss_detail.as_str(),
|
||||
beautify_local_execution_client_error_message(
|
||||
local_execution_runtime_miss_detail.as_str(),
|
||||
)
|
||||
.as_str(),
|
||||
)?;
|
||||
let local_execution_runtime_miss_reason = local_execution_runtime_miss_diagnostic
|
||||
.as_ref()
|
||||
|
||||
@@ -562,7 +562,7 @@ async fn gateway_surfaces_candidate_list_empty_reason_for_claude_chat_runtime_mi
|
||||
assert_eq!(payload["error"]["type"], "http_error");
|
||||
assert_eq!(
|
||||
payload["error"]["message"],
|
||||
"没有可用提供商支持模型 claude-sonnet-4-5 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)"
|
||||
"没有可用提供商支持模型 claude-sonnet-4-5 的同步请求"
|
||||
);
|
||||
|
||||
let stored_candidates = request_candidate_repository
|
||||
|
||||
@@ -958,7 +958,7 @@ async fn gateway_marks_claude_cli_cross_format_runtime_miss_when_format_conversi
|
||||
assert_eq!(response_json["error"]["type"], "http_error");
|
||||
assert_eq!(
|
||||
response_json["error"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)"
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求"
|
||||
);
|
||||
|
||||
let stored_candidates = request_candidate_repository
|
||||
|
||||
@@ -955,7 +955,7 @@ async fn gateway_records_failed_usage_for_claude_runtime_miss_without_execution_
|
||||
assert_eq!(body_json["error"]["type"], "http_error");
|
||||
assert_eq!(
|
||||
body_json["error"]["message"],
|
||||
"没有可用提供商支持模型 claude-sonnet-4-5 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)"
|
||||
"没有可用提供商支持模型 claude-sonnet-4-5 的同步请求"
|
||||
);
|
||||
|
||||
let stored_usage = wait_for_usage_status(
|
||||
@@ -1576,7 +1576,7 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
|
||||
assert_eq!(body_json["error"]["type"], "http_error");
|
||||
assert_eq!(
|
||||
body_json["error"]["message"],
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)"
|
||||
"没有可用提供商支持模型 gpt-5.4 的同步请求"
|
||||
);
|
||||
|
||||
let stored_usage = wait_for_usage_status(
|
||||
|
||||
@@ -504,7 +504,7 @@ fn admin_usage_error_domains_json(item: &StoredRequestUsageAudit) -> Value {
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
let client_error = admin_usage_error_domain_json(
|
||||
let mut client_error = admin_usage_error_domain_json(
|
||||
"client_response",
|
||||
item.status_code,
|
||||
item.client_response_headers.as_ref(),
|
||||
@@ -512,6 +512,17 @@ fn admin_usage_error_domains_json(item: &StoredRequestUsageAudit) -> Value {
|
||||
item.error_category.as_deref(),
|
||||
item.error_message.as_deref(),
|
||||
);
|
||||
if item.routing_execution_path() == Some("local_execution_runtime_miss") {
|
||||
if let (Some(object), Some(message)) = (
|
||||
client_error.as_object_mut(),
|
||||
item.error_message
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty()),
|
||||
) {
|
||||
object.insert("message".to_string(), Value::String(message.to_string()));
|
||||
}
|
||||
}
|
||||
let request_error = Value::Null;
|
||||
let failure_summary =
|
||||
admin_usage_failure_summary_json(item, &request_error, &upstream_error, &client_error);
|
||||
@@ -2682,17 +2693,20 @@ mod tests {
|
||||
#[test]
|
||||
fn detail_payload_does_not_promote_local_client_error_to_upstream_error() {
|
||||
let message = "没有可用提供商支持模型 gpt-5.4 的同步请求。请检查模型映射、端点启用状态和 API Key 权限(原因代码: candidate_list_empty)";
|
||||
let client_message = "没有可用提供商支持模型 gpt-5.4 的同步请求";
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_api_key_id: None,
|
||||
provider_request_headers: None,
|
||||
provider_request_body: None,
|
||||
provider_request_body_ref: None,
|
||||
candidate_id: None,
|
||||
execution_path: Some("local_execution_runtime_miss".to_string()),
|
||||
local_execution_runtime_miss_reason: Some("candidate_list_empty".to_string()),
|
||||
error_category: Some("http_error".to_string()),
|
||||
client_response_body: Some(json!({
|
||||
"error": {
|
||||
"type": "http_error",
|
||||
"message": message
|
||||
"message": client_message
|
||||
}
|
||||
})),
|
||||
response_body: Some(json!({
|
||||
@@ -2718,6 +2732,10 @@ mod tests {
|
||||
|
||||
assert!(payload["upstream_error"].is_null());
|
||||
assert_eq!(payload["client_error"]["message"], message);
|
||||
assert_eq!(
|
||||
payload["client_response_body"]["error"]["message"],
|
||||
client_message
|
||||
);
|
||||
assert_eq!(payload["error_flow"]["source"], "gateway");
|
||||
assert_eq!(payload["error_flow"]["propagation"], "local");
|
||||
}
|
||||
|
||||
@@ -820,23 +820,6 @@ function formatErrorDomainMeta(domain: NormalizedErrorDomain): string {
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
function simplifyClientErrorMessage(message: string): string {
|
||||
let simplified = message.trim()
|
||||
if (!simplified) return ''
|
||||
|
||||
simplified = simplified
|
||||
.replace(/[((]\s*原因代码\s*[::][^))]*[))]/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
const advisoryIndex = simplified.search(/[。.!?]\s*(请检查|请确认|原因代码|Reason|Code)/i)
|
||||
if (advisoryIndex > 0) {
|
||||
simplified = simplified.slice(0, advisoryIndex)
|
||||
}
|
||||
|
||||
return simplified.replace(/[。.!?;;,,::\s]+$/u, '').trim()
|
||||
}
|
||||
|
||||
function handleTraceState(state: { loaded: boolean, hasTrace: boolean }) {
|
||||
timelineLoaded.value = state.loaded
|
||||
timelineHasTrace.value = state.hasTrace
|
||||
@@ -968,7 +951,7 @@ const normalizedUpstreamError = computed(() =>
|
||||
)
|
||||
|
||||
const displayClientErrorMessage = computed(() =>
|
||||
normalizedClientError.value ? simplifyClientErrorMessage(normalizedClientError.value.message) : '',
|
||||
normalizedClientError.value?.message ?? '',
|
||||
)
|
||||
|
||||
const hasVisibleErrorCards = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user