fix(usage): 简化本地执行错误提示

This commit is contained in:
fawney19
2026-04-25 20:46:22 +08:00
parent 912a92cd1a
commit 429fdb47e6
8 changed files with 119 additions and 34 deletions
+4 -3
View File
@@ -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,
+82 -3
View File
@@ -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
+2 -2
View File
@@ -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(
+20 -2
View File
@@ -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(() =>