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

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,

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();

View File

@@ -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()

View File

@@ -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

View File

@@ -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

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(