e6f0e6248e
空响应纳入有界重试,并为优化请求设置输出 token 上限。 修复题材提示词括号并补充回归测试。 将优化请求身份更新移入已提交 effect,避免并发渲染污染。 移除冗余类型注释并同步编辑器文档。
186 lines
6.4 KiB
Rust
186 lines
6.4 KiB
Rust
use axum::http::{HeaderValue, StatusCode};
|
|
use platform_auth::{AuthPlatformErrorKind, WechatProviderError};
|
|
use platform_llm::{LlmError, LlmErrorKind};
|
|
use platform_oss::{OssError, OssErrorKind};
|
|
use platform_wechat::{WechatError, WechatErrorKind};
|
|
use serde_json::json;
|
|
|
|
use crate::http_error::AppError;
|
|
|
|
// API 层统一消费 platform 的稳定错误分类,避免各 route 重复 match 具体 provider 分支。
|
|
pub fn map_llm_error(error: LlmError) -> AppError {
|
|
let message = llm_error_message(&error);
|
|
let status = match error.kind() {
|
|
LlmErrorKind::InvalidRequest => StatusCode::BAD_REQUEST,
|
|
LlmErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
|
|
LlmErrorKind::Upstream
|
|
if matches!(
|
|
error,
|
|
LlmError::Upstream {
|
|
status_code: 429,
|
|
..
|
|
}
|
|
) =>
|
|
{
|
|
StatusCode::TOO_MANY_REQUESTS
|
|
}
|
|
LlmErrorKind::Timeout
|
|
| LlmErrorKind::Connectivity
|
|
| LlmErrorKind::Upstream
|
|
| LlmErrorKind::StreamUnavailable
|
|
| LlmErrorKind::EmptyResponse
|
|
| LlmErrorKind::Transport
|
|
| LlmErrorKind::Deserialize => StatusCode::BAD_GATEWAY,
|
|
};
|
|
|
|
AppError::from_status(status).with_message(message)
|
|
}
|
|
|
|
pub(crate) fn is_retryable_llm_error(error: &LlmError) -> bool {
|
|
match error {
|
|
LlmError::Timeout { .. }
|
|
| LlmError::Connectivity { .. }
|
|
| LlmError::Transport(_)
|
|
| LlmError::EmptyResponse => true,
|
|
LlmError::Upstream { status_code, .. } => {
|
|
matches!(*status_code, 408 | 429 | 500..=599)
|
|
}
|
|
LlmError::InvalidConfig(_)
|
|
| LlmError::InvalidRequest(_)
|
|
| LlmError::StreamUnavailable
|
|
| LlmError::Deserialize(_) => false,
|
|
}
|
|
}
|
|
|
|
pub fn map_oss_error(error: OssError, provider: &'static str) -> AppError {
|
|
let status = oss_error_status(error.kind());
|
|
AppError::from_status(status).with_details(json!({
|
|
"provider": provider,
|
|
"message": error.to_string(),
|
|
}))
|
|
}
|
|
|
|
pub fn map_phone_auth_platform_store_error(message: String) -> AppError {
|
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
|
|
}
|
|
|
|
pub fn map_wechat_provider_error(error: WechatProviderError) -> AppError {
|
|
let status = match error.kind() {
|
|
AuthPlatformErrorKind::Disabled
|
|
| AuthPlatformErrorKind::MissingCode
|
|
| AuthPlatformErrorKind::InvalidCallback => StatusCode::BAD_REQUEST,
|
|
AuthPlatformErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
|
|
AuthPlatformErrorKind::RequestFailed
|
|
| AuthPlatformErrorKind::DeserializeFailed
|
|
| AuthPlatformErrorKind::MissingProfile
|
|
| AuthPlatformErrorKind::Upstream => StatusCode::BAD_GATEWAY,
|
|
AuthPlatformErrorKind::InvalidClaims
|
|
| AuthPlatformErrorKind::SignFailed
|
|
| AuthPlatformErrorKind::VerifyFailed
|
|
| AuthPlatformErrorKind::CookieConfig
|
|
| AuthPlatformErrorKind::HashFailed
|
|
| AuthPlatformErrorKind::InvalidVerifyCode => StatusCode::INTERNAL_SERVER_ERROR,
|
|
};
|
|
|
|
AppError::from_status(status).with_message(error.to_string())
|
|
}
|
|
|
|
pub fn map_wechat_error(error: WechatError) -> AppError {
|
|
let status = match error.kind() {
|
|
WechatErrorKind::InvalidConfig => StatusCode::SERVICE_UNAVAILABLE,
|
|
WechatErrorKind::RequestFailed
|
|
| WechatErrorKind::DeserializeFailed
|
|
| WechatErrorKind::Upstream => StatusCode::BAD_GATEWAY,
|
|
};
|
|
|
|
AppError::from_status(status).with_message(error.to_string())
|
|
}
|
|
|
|
pub fn attach_retry_after(error: AppError, retry_after_seconds: u64) -> AppError {
|
|
match HeaderValue::from_str(&retry_after_seconds.to_string()) {
|
|
Ok(value) => error.with_header("retry-after", value),
|
|
Err(_) => error,
|
|
}
|
|
}
|
|
|
|
fn oss_error_status(kind: OssErrorKind) -> StatusCode {
|
|
match kind {
|
|
OssErrorKind::InvalidConfig | OssErrorKind::InvalidRequest => StatusCode::BAD_REQUEST,
|
|
OssErrorKind::ObjectNotFound => StatusCode::NOT_FOUND,
|
|
OssErrorKind::Request | OssErrorKind::SerializePolicy | OssErrorKind::Sign => {
|
|
StatusCode::BAD_GATEWAY
|
|
}
|
|
}
|
|
}
|
|
|
|
fn llm_error_message(error: &LlmError) -> String {
|
|
match error {
|
|
LlmError::InvalidConfig(message)
|
|
| LlmError::InvalidRequest(message)
|
|
| LlmError::Transport(message)
|
|
| LlmError::Deserialize(message) => message.clone(),
|
|
LlmError::Timeout { .. }
|
|
| LlmError::Connectivity { .. }
|
|
| LlmError::Upstream { .. }
|
|
| LlmError::StreamUnavailable
|
|
| LlmError::EmptyResponse => error.to_string(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn map_oss_error_uses_stable_kind_for_not_found() {
|
|
let error = map_oss_error(
|
|
OssError::ObjectNotFound("missing object".to_string()),
|
|
"oss",
|
|
);
|
|
|
|
assert_eq!(error.status_code(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[test]
|
|
fn map_llm_error_preserves_upstream_rate_limit() {
|
|
let error = map_llm_error(LlmError::Upstream {
|
|
status_code: 429,
|
|
message: "too many requests".to_string(),
|
|
});
|
|
|
|
assert_eq!(error.status_code(), StatusCode::TOO_MANY_REQUESTS);
|
|
}
|
|
|
|
#[test]
|
|
fn llm_business_retries_only_transient_failures() {
|
|
assert!(is_retryable_llm_error(&LlmError::Timeout { attempts: 1 }));
|
|
assert!(is_retryable_llm_error(&LlmError::Upstream {
|
|
status_code: 429,
|
|
message: "rate limited".to_string(),
|
|
}));
|
|
assert!(is_retryable_llm_error(&LlmError::Upstream {
|
|
status_code: 503,
|
|
message: "unavailable".to_string(),
|
|
}));
|
|
assert!(is_retryable_llm_error(&LlmError::EmptyResponse));
|
|
assert!(!is_retryable_llm_error(&LlmError::InvalidConfig(
|
|
"missing key".to_string(),
|
|
)));
|
|
assert!(!is_retryable_llm_error(&LlmError::Upstream {
|
|
status_code: 401,
|
|
message: "unauthorized".to_string(),
|
|
}));
|
|
assert!(!is_retryable_llm_error(&LlmError::Deserialize(
|
|
"invalid response".to_string(),
|
|
)));
|
|
}
|
|
|
|
#[test]
|
|
fn map_wechat_provider_error_keeps_provider_boundary() {
|
|
let error = map_wechat_provider_error(WechatProviderError::MissingCode);
|
|
|
|
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
|
assert_eq!(error.message(), "缺少微信授权 code");
|
|
}
|
|
}
|