fix(proxy): resolve clippy warnings for dead code and uninlined format args

- Add #[allow(dead_code)] to unused ProviderUnhealthy variant
- Inline format string arguments in handlers.rs and codex.rs log macros
- Refactor error response handling to properly pass through upstream errors
- Add URL deduplication logic for /v1/v1 paths in CodexAdapter
This commit is contained in:
YoVinchen
2025-12-01 15:58:30 +08:00
parent 0eec47fd64
commit 665d34609d
3 changed files with 389 additions and 32 deletions
+76 -26
View File
@@ -23,6 +23,7 @@ pub enum ProxyError {
#[error("无可用的Provider")]
NoAvailableProvider,
#[allow(dead_code)]
#[error("Provider不健康: {0}")]
ProviderUnhealthy(String),
@@ -56,34 +57,83 @@ pub enum ProxyError {
impl IntoResponse for ProxyError {
fn into_response(self) -> Response {
let (status, message) = match &self {
ProxyError::AlreadyRunning => (StatusCode::CONFLICT, self.to_string()),
ProxyError::NotRunning => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
ProxyError::BindFailed(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ProxyError::ForwardFailed(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
ProxyError::NoAvailableProvider => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
ProxyError::ProviderUnhealthy(_) => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
ProxyError::UpstreamError { status, .. } => (
StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_GATEWAY),
self.to_string(),
),
ProxyError::MaxRetriesExceeded => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
ProxyError::DatabaseError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
ProxyError::ConfigError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ProxyError::TransformError(_) => (StatusCode::UNPROCESSABLE_ENTITY, self.to_string()),
ProxyError::InvalidRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ProxyError::Timeout(_) => (StatusCode::GATEWAY_TIMEOUT, self.to_string()),
ProxyError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
let (status, body) = match &self {
ProxyError::UpstreamError {
status: upstream_status,
body: upstream_body,
} => {
let http_status =
StatusCode::from_u16(*upstream_status).unwrap_or(StatusCode::BAD_GATEWAY);
// 尝试解析上游响应体为 JSON,如果失败则包装为字符串
let error_body = if let Some(body_str) = upstream_body {
if let Ok(json_body) = serde_json::from_str::<serde_json::Value>(body_str) {
// 上游返回的是 JSON,直接透传
json_body
} else {
// 上游返回的不是 JSON,包装为错误消息
json!({
"error": {
"message": body_str,
"type": "upstream_error",
}
})
}
} else {
json!({
"error": {
"message": format!("Upstream error (status {})", upstream_status),
"type": "upstream_error",
}
})
};
(http_status, error_body)
}
_ => {
let (http_status, message) = match &self {
ProxyError::AlreadyRunning => (StatusCode::CONFLICT, self.to_string()),
ProxyError::NotRunning => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
ProxyError::BindFailed(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
}
ProxyError::ForwardFailed(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
ProxyError::NoAvailableProvider => {
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
}
ProxyError::ProviderUnhealthy(_) => {
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
}
ProxyError::MaxRetriesExceeded => {
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
}
ProxyError::DatabaseError(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
}
ProxyError::ConfigError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ProxyError::TransformError(_) => {
(StatusCode::UNPROCESSABLE_ENTITY, self.to_string())
}
ProxyError::InvalidRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ProxyError::Timeout(_) => (StatusCode::GATEWAY_TIMEOUT, self.to_string()),
ProxyError::Internal(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
}
ProxyError::UpstreamError { .. } => unreachable!(),
};
let error_body = json!({
"error": {
"message": message,
"type": "proxy_error",
}
});
(http_status, error_body)
}
};
let body = Json(json!({
"error": {
"message": message,
"type": "proxy_error",
}
}));
(status, body).into_response()
(status, Json(body)).into_response()
}
}
+98 -6
View File
@@ -2,7 +2,13 @@
//!
//! 处理各种API端点的HTTP请求
use super::{forwarder::RequestForwarder, server::ProxyState, types::*, ProxyError};
use super::{
forwarder::RequestForwarder,
providers::{get_adapter, transform},
server::ProxyState,
types::*,
ProxyError,
};
use crate::app_config::AppType;
use axum::{extract::State, http::StatusCode, Json};
use serde_json::{json, Value};
@@ -35,11 +41,27 @@ pub async fn handle_messages(
// 选择目标 Provider
let router = super::router::ProviderRouter::new(state.db.clone());
let failed_ids = Vec::new();
let _provider = router
let provider = router
.select_provider(&AppType::Claude, &failed_ids)
.await?;
// 直接透传 Claude 请求
// 检查是否需要转换(OpenRouter)
let adapter = get_adapter(&AppType::Claude);
let needs_transform = adapter.needs_transform(&provider);
// 检查是否是流式请求
let is_stream = body
.get("stream")
.and_then(|s| s.as_bool())
.unwrap_or(false);
log::info!(
"[Claude] Provider: {}, needs_transform: {}, is_stream: {}",
provider.name,
needs_transform,
is_stream
);
let forwarder = RequestForwarder::new(
state.db.clone(),
config.request_timeout,
@@ -51,16 +73,86 @@ pub async fn handle_messages(
.forward_with_retry(&AppType::Claude, "/v1/messages", body, headers)
.await?;
// 透传响应
let status = response.status();
log::info!("[Claude] 上游响应状态: {status}");
// 如果需要转换且是非流式请求,转换响应
if needs_transform && !is_stream {
log::info!("[Claude] 开始转换响应 (OpenAI → Anthropic)");
let response_headers = response.headers().clone();
// 读取响应体
let body_bytes = response.bytes().await.map_err(|e| {
log::error!("[Claude] 读取响应体失败: {e}");
ProxyError::ForwardFailed(format!("Failed to read response body: {e}"))
})?;
let body_str = String::from_utf8_lossy(&body_bytes);
log::info!("[Claude] OpenAI 响应长度: {} bytes", body_bytes.len());
log::debug!("[Claude] OpenAI 原始响应: {body_str}");
// 解析并转换
let openai_response: Value = serde_json::from_slice(&body_bytes).map_err(|e| {
log::error!("[Claude] 解析 OpenAI 响应失败: {e}, body: {body_str}");
ProxyError::TransformError(format!("Failed to parse OpenAI response: {e}"))
})?;
log::info!("[Claude] 解析 OpenAI 响应成功");
let anthropic_response = transform::openai_to_anthropic(openai_response).map_err(|e| {
log::error!("[Claude] 转换响应失败: {e}");
e
})?;
log::info!("[Claude] 转换响应成功");
log::info!(
"[Claude] Anthropic 响应: {}",
serde_json::to_string(&anthropic_response).unwrap_or_default()
);
// 构建响应
let mut builder = axum::response::Response::builder().status(status);
// 复制响应头(排除 content-length,因为内容已改变)
for (key, value) in response_headers.iter() {
if key.as_str().to_lowercase() != "content-length"
&& key.as_str().to_lowercase() != "transfer-encoding"
{
builder = builder.header(key, value);
}
}
builder = builder.header("content-type", "application/json");
let response_body = serde_json::to_vec(&anthropic_response).map_err(|e| {
log::error!("[Claude] 序列化响应失败: {e}");
ProxyError::TransformError(format!("Failed to serialize response: {e}"))
})?;
log::info!(
"[Claude] 返回转换后的响应, 长度: {} bytes",
response_body.len()
);
let body = axum::body::Body::from(response_body);
return Ok(builder.body(body).unwrap());
}
// 流式请求需要特殊处理
if needs_transform && is_stream {
log::warn!("[Claude] OpenRouter 流式请求暂不支持完整转换,透传响应");
}
// 透传响应(直连 Anthropic 或流式请求)
log::info!("[Claude] 透传响应");
let mut builder = axum::response::Response::builder().status(response.status());
// 复制响应头
for (key, value) in response.headers() {
builder = builder.header(key, value);
}
let body = axum::body::Body::from_stream(response.bytes_stream());
Ok(builder.body(body).unwrap())
}
+215
View File
@@ -0,0 +1,215 @@
//! Codex (OpenAI) Provider Adapter
//!
//! 仅透传模式,支持直连 OpenAI API
use super::{AuthInfo, AuthStrategy, ProviderAdapter};
use crate::provider::Provider;
use crate::proxy::error::ProxyError;
use reqwest::RequestBuilder;
/// Codex 适配器
pub struct CodexAdapter;
impl CodexAdapter {
pub fn new() -> Self {
Self
}
/// 从 Provider 配置中提取 API Key
fn extract_key(&self, provider: &Provider) -> Option<String> {
// 1. 尝试从 env 中获取
if let Some(env) = provider.settings_config.get("env") {
if let Some(key) = env.get("OPENAI_API_KEY").and_then(|v| v.as_str()) {
return Some(key.to_string());
}
}
// 2. 尝试从 auth 中获取 (Codex CLI 格式)
if let Some(auth) = provider.settings_config.get("auth") {
if let Some(key) = auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) {
return Some(key.to_string());
}
}
// 3. 尝试直接获取
if let Some(key) = provider
.settings_config
.get("apiKey")
.or_else(|| provider.settings_config.get("api_key"))
.and_then(|v| v.as_str())
{
return Some(key.to_string());
}
// 4. 尝试从 config 对象中获取
if let Some(config) = provider.settings_config.get("config") {
if let Some(key) = config
.get("api_key")
.or_else(|| config.get("apiKey"))
.and_then(|v| v.as_str())
{
return Some(key.to_string());
}
}
None
}
}
impl Default for CodexAdapter {
fn default() -> Self {
Self::new()
}
}
impl ProviderAdapter for CodexAdapter {
fn name(&self) -> &'static str {
"Codex"
}
fn extract_base_url(&self, provider: &Provider) -> Result<String, ProxyError> {
// 1. 尝试直接获取 base_url 字段
if let Some(url) = provider
.settings_config
.get("base_url")
.and_then(|v| v.as_str())
{
return Ok(url.trim_end_matches('/').to_string());
}
// 2. 尝试 baseURL
if let Some(url) = provider
.settings_config
.get("baseURL")
.and_then(|v| v.as_str())
{
return Ok(url.trim_end_matches('/').to_string());
}
// 3. 尝试从 config 对象中获取
if let Some(config) = provider.settings_config.get("config") {
if let Some(url) = config.get("base_url").and_then(|v| v.as_str()) {
return Ok(url.trim_end_matches('/').to_string());
}
// 尝试解析 TOML 字符串格式
if let Some(config_str) = config.as_str() {
if let Some(start) = config_str.find("base_url = \"") {
let rest = &config_str[start + 12..];
if let Some(end) = rest.find('"') {
return Ok(rest[..end].trim_end_matches('/').to_string());
}
}
if let Some(start) = config_str.find("base_url = '") {
let rest = &config_str[start + 12..];
if let Some(end) = rest.find('\'') {
return Ok(rest[..end].trim_end_matches('/').to_string());
}
}
}
}
Err(ProxyError::ConfigError(
"Codex Provider 缺少 base_url 配置".to_string(),
))
}
fn extract_auth(&self, provider: &Provider) -> Option<AuthInfo> {
self.extract_key(provider)
.map(|key| AuthInfo::new(key, AuthStrategy::Bearer))
}
fn build_url(&self, base_url: &str, endpoint: &str) -> String {
let base_trimmed = base_url.trim_end_matches('/');
let endpoint_trimmed = endpoint.trim_start_matches('/');
let mut url = format!("{base_trimmed}/{endpoint_trimmed}");
// 去除重复的 /v1/v1
if url.contains("/v1/v1") {
url = url.replace("/v1/v1", "/v1");
}
url
}
fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder {
request.header("Authorization", format!("Bearer {}", auth.api_key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_provider(config: serde_json::Value) -> Provider {
Provider {
id: "test".to_string(),
name: "Test Codex".to_string(),
settings_config: config,
website_url: None,
category: Some("codex".to_string()),
created_at: None,
sort_index: None,
notes: None,
meta: None,
icon: None,
icon_color: None,
is_proxy_target: None,
}
}
#[test]
fn test_extract_base_url_direct() {
let adapter = CodexAdapter::new();
let provider = create_provider(json!({
"base_url": "https://api.openai.com/v1"
}));
let url = adapter.extract_base_url(&provider).unwrap();
assert_eq!(url, "https://api.openai.com/v1");
}
#[test]
fn test_extract_auth_from_auth_field() {
let adapter = CodexAdapter::new();
let provider = create_provider(json!({
"auth": {
"OPENAI_API_KEY": "sk-test-key-12345678"
}
}));
let auth = adapter.extract_auth(&provider).unwrap();
assert_eq!(auth.api_key, "sk-test-key-12345678");
assert_eq!(auth.strategy, AuthStrategy::Bearer);
}
#[test]
fn test_extract_auth_from_env() {
let adapter = CodexAdapter::new();
let provider = create_provider(json!({
"env": {
"OPENAI_API_KEY": "sk-env-key-12345678"
}
}));
let auth = adapter.extract_auth(&provider).unwrap();
assert_eq!(auth.api_key, "sk-env-key-12345678");
}
#[test]
fn test_build_url() {
let adapter = CodexAdapter::new();
let url = adapter.build_url("https://api.openai.com/v1", "/responses");
assert_eq!(url, "https://api.openai.com/v1/responses");
}
#[test]
fn test_build_url_dedup_v1() {
let adapter = CodexAdapter::new();
// base_url 已包含 /v1endpoint 也包含 /v1
let url = adapter.build_url("https://www.packyapi.com/v1", "/v1/responses");
assert_eq!(url, "https://www.packyapi.com/v1/responses");
}
}