feat: 智能降级凭证选择 - Provider Pool 回退至 API Key Provider

当 Provider Pool 中没有可用凭证时,自动降级到 API Key Provider 获取凭证:

- 在 AppState 中添加 api_key_service 字段支持智能降级
- 在 provider_pool_service.rs 中添加 select_credential_with_fallback 方法
- 在 api_key_provider_service.rs 中添加 map_pool_type_to_api_type 类型映射
- 更新 /v1/chat/completions 端点支持多源凭证选择
- 支持 OAuth、API Key、Plugin 三种凭证来源的智能选择

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pengshiyu
2026-01-05 19:56:05 +08:00
co-authored by Claude Opus 4.5
parent 373ea7da8c
commit 0a99125ba9
10 changed files with 450 additions and 83 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "proxycast",
"version": "0.23.0",
"version": "0.25.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "proxycast",
"version": "0.23.0",
"version": "0.25.0",
"dependencies": {
"@fabianlars/tauri-plugin-oauth": "^2",
"@radix-ui/react-collapsible": "^1.1.12",
File diff suppressed because one or more lines are too long
-54
View File
@@ -2396,60 +2396,6 @@
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`",
"type": "string",
"const": "deep-link:default",
"markdownDescription": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`"
},
{
"description": "Enables the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-get-current",
"markdownDescription": "Enables the get_current command without any pre-configured scope."
},
{
"description": "Enables the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-is-registered",
"markdownDescription": "Enables the is_registered command without any pre-configured scope."
},
{
"description": "Enables the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-register",
"markdownDescription": "Enables the register command without any pre-configured scope."
},
{
"description": "Enables the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:allow-unregister",
"markdownDescription": "Enables the unregister command without any pre-configured scope."
},
{
"description": "Denies the get_current command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-get-current",
"markdownDescription": "Denies the get_current command without any pre-configured scope."
},
{
"description": "Denies the is_registered command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-is-registered",
"markdownDescription": "Denies the is_registered command without any pre-configured scope."
},
{
"description": "Denies the register command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-register",
"markdownDescription": "Denies the register command without any pre-configured scope."
},
{
"description": "Denies the unregister command without any pre-configured scope.",
"type": "string",
"const": "deep-link:deny-unregister",
"markdownDescription": "Denies the unregister command without any pre-configured scope."
},
{
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
"type": "string",
+9 -3
View File
@@ -783,7 +783,7 @@ pub async fn chat_completions(
}
};
// 如果 Provider Pool 中没有找到凭证,尝试从 API Key Provider 获取
// 如果 Provider Pool 中没有找到凭证,尝试从 API Key Provider 获取(智能降级)
let credential = if credential.is_none() {
eprintln!("[CHAT_COMPLETIONS] Provider Pool 中未找到凭证,尝试 API Key Provider...");
@@ -1714,13 +1714,19 @@ pub async fn anthropic_messages(
),
);
// 尝试从凭证池中选择凭证
// 尝试从凭证池中选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => {
// 根据选择的 Provider 配置选择凭证
state
.pool_service
.select_credential(db, &selected_provider, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&selected_provider,
Some(&request.model),
None, // provider_id_hint 可从路由或请求头提取
)
.ok()
.flatten()
}
@@ -127,7 +127,7 @@ pub async fn credentials_select(
}
}
// 尝试从 API Key Provider 选择
// 尝试从 API Key Provider 选择(智能降级)
if source_pref.is_none() || source_pref == Some("api_key") {
if let Some(response) = try_select_api_key_credential(&state, db, &request).await? {
return Ok(Json(response));
@@ -144,7 +144,7 @@ pub async fn credentials_select(
// 没有找到可用凭证
Err(CredentialApiError {
error: "no_available_credentials".to_string(),
message: format!("没有可用的 {} 凭证", request.provider_type),
message: format!("没有可用的 {} 凭证。您可以在 API Key Provider 中配置 API Key 作为降级选项。", request.provider_type),
status_code: 503,
})
}
@@ -248,7 +248,7 @@ pub async fn select_credential(
status_code: 404,
})?
} else {
// 智能选择最优凭证
// 智能选择最优凭证(Kiro 是 OAuth-only,不支持降级到 API Key)
state
.pool_service
.select_credential(db, "kiro", request.model.as_deref())
@@ -259,7 +259,7 @@ pub async fn select_credential(
})?
.ok_or_else(|| ApiError {
error: "no_available_credentials".to_string(),
message: "没有可用的kiro凭证".to_string(),
message: "没有可用的 Kiro 凭证。Kiro 仅支持 OAuth 认证,无法降级到 API Key。".to_string(),
status_code: 503,
})?
};
+16 -4
View File
@@ -459,11 +459,17 @@ async fn handle_ws_chat_completions(
// 获取默认 provider
let default_provider = state.default_provider.read().await.clone();
// 尝试从凭证池中选择凭证
// 尝试从凭证池中选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => state
.pool_service
.select_credential(db, &default_provider, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&default_provider,
Some(&request.model),
None, // provider_id_hint
)
.ok()
.flatten(),
None => None,
@@ -592,11 +598,17 @@ async fn handle_ws_anthropic_messages(
// 获取默认 provider
let default_provider = state.default_provider.read().await.clone();
// 尝试从凭证池中选择凭证
// 尝试从凭证池中选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => state
.pool_service
.select_credential(db, &default_provider, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&default_provider,
Some(&request.model),
None, // provider_id_hint
)
.ok()
.flatten(),
None => None,
+45 -15
View File
@@ -394,7 +394,7 @@ pub struct AppState {
pub endpoint_providers: Arc<RwLock<EndpointProvidersConfig>>,
/// Kiro 事件服务
pub kiro_event_service: Arc<KiroEventService>,
/// API Key Provider 服务
/// API Key Provider 服务(用于智能降级)
pub api_key_service: Arc<crate::services::api_key_provider_service::ApiKeyProviderService>,
}
@@ -1008,11 +1008,17 @@ async fn gemini_generate_content(
// 获取默认 provider
let default_provider = state.default_provider.read().await.clone();
// 尝试从凭证池中选择 Antigravity 凭证
// 尝试从凭证池中选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => state
.pool_service
.select_credential(db, &default_provider, Some(model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&default_provider,
Some(model),
None, // provider_id_hint
)
.ok()
.flatten(),
None => None,
@@ -1025,7 +1031,7 @@ async fn gemini_generate_content(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": {
"message": "没有可用的 Antigravity 凭证,请先添加凭证"
"message": "没有可用的凭证。您可以在 API Key Provider 中配置 API Key 作为降级选项。"
}
})),
)
@@ -1284,7 +1290,7 @@ async fn anthropic_messages_with_selector(
),
);
// 尝试解析凭证
// 尝试解析凭证(带智能降级)
let credential = match &state.db {
Some(db) => {
// 首先尝试按名称查找
@@ -1295,11 +1301,17 @@ async fn anthropic_messages_with_selector(
else if let Ok(Some(cred)) = state.pool_service.get_by_uuid(db, &selector) {
Some(cred)
}
// 最后尝试按 provider 类型轮询
// 最后尝试按 provider 类型轮询(带智能降级)
else if let Ok(Some(cred)) =
state
.pool_service
.select_credential(db, &selector, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&selector,
Some(&request.model),
None, // provider_id_hint
)
{
Some(cred)
} else {
@@ -1363,7 +1375,7 @@ async fn chat_completions_with_selector(
),
);
// 尝试解析凭证
// 尝试解析凭证(带智能降级)
let credential = match &state.db {
Some(db) => {
if let Ok(Some(cred)) = state.pool_service.get_by_name(db, &selector) {
@@ -1373,7 +1385,13 @@ async fn chat_completions_with_selector(
} else if let Ok(Some(cred)) =
state
.pool_service
.select_credential(db, &selector, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&selector,
Some(&request.model),
None, // provider_id_hint
)
{
Some(cred)
} else {
@@ -1456,14 +1474,20 @@ async fn amp_chat_completions(
),
);
// 尝试根据 provider 名称选择凭证
// 尝试根据 provider 名称选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => {
// 首先尝试按 provider 类型选择
// 首先尝试按 provider 类型选择(带智能降级)
if let Ok(Some(cred)) =
state
.pool_service
.select_credential(db, &provider, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&provider,
Some(&request.model),
Some(&provider), // provider_id_hint 使用路由中的 provider 名称
)
{
Some(cred)
}
@@ -1552,14 +1576,20 @@ async fn amp_messages(
),
);
// 尝试根据 provider 名称选择凭证
// 尝试根据 provider 名称选择凭证(带智能降级)
let credential = match &state.db {
Some(db) => {
// 首先尝试按 provider 类型选择
// 首先尝试按 provider 类型选择(带智能降级)
if let Ok(Some(cred)) =
state
.pool_service
.select_credential(db, &provider, Some(&request.model))
.select_credential_with_fallback(
db,
&state.api_key_service,
&provider,
Some(&request.model),
Some(&provider), // provider_id_hint 使用路由中的 provider 名称
)
{
Some(cred)
}
@@ -670,6 +670,317 @@ impl ApiKeyProviderService {
errors,
})
}
// ==================== 智能降级 ====================
/// 根据 PoolProviderType 获取降级凭证
///
/// 用于智能降级场景:当 Provider Pool 无可用凭证时,自动从 API Key Provider 查找
///
/// 降级策略:
/// 1. 首先通过类型映射查找 (PoolProviderType → ApiProviderType)
/// 2. 如果类型映射失败,尝试通过 provider_id 直接查找 (支持 60+ Provider)
///
/// # 参数
/// - `db`: 数据库连接
/// - `pool_type`: Provider Pool 中的 Provider 类型
/// - `provider_id_hint`: 可选的 provider_id 提示,如 "deepseek", "dashscope"
///
/// # 返回
/// - `Ok(Some(credential))`: 找到可用的降级凭证
/// - `Ok(None)`: 没有找到可用的降级凭证
/// - `Err(e)`: 查询过程中发生错误
pub fn get_fallback_credential(
&self,
db: &DbConnection,
pool_type: &PoolProviderType,
provider_id_hint: Option<&str>,
) -> Result<Option<ProviderCredential>, String> {
// 策略 1: 通过类型映射查找
if let Some(api_type) = self.map_pool_type_to_api_type(pool_type) {
tracing::debug!(
"[智能降级] 尝试类型映射: {:?} -> {:?}",
pool_type,
api_type
);
if let Some(cred) = self.find_by_api_type(db, pool_type, &api_type)? {
return Ok(Some(cred));
}
}
// 策略 2: 通过 provider_id 直接查找 (支持 60+ Provider)
if let Some(provider_id) = provider_id_hint {
tracing::debug!(
"[智能降级] 尝试 provider_id 查找: {}",
provider_id
);
if let Some(cred) = self.find_by_provider_id(db, provider_id)? {
return Ok(Some(cred));
}
}
tracing::debug!(
"[智能降级] 未找到 {:?} 的降级凭证 (provider_id_hint: {:?})",
pool_type,
provider_id_hint
);
Ok(None)
}
/// PoolProviderType → ApiProviderType 映射
///
/// 仅映射有明确对应关系的类型
fn map_pool_type_to_api_type(
&self,
pool_type: &PoolProviderType,
) -> Option<ApiProviderType> {
match pool_type {
// API Key 类型 - 直接映射
PoolProviderType::Claude => Some(ApiProviderType::Anthropic),
PoolProviderType::OpenAI => Some(ApiProviderType::Openai),
PoolProviderType::GeminiApiKey => Some(ApiProviderType::Gemini),
PoolProviderType::Vertex => Some(ApiProviderType::Vertexai),
// OAuth 类型 - 可降级到 API Key
PoolProviderType::Gemini => Some(ApiProviderType::Gemini), // Gemini OAuth → Gemini API Key
PoolProviderType::Qwen => Some(ApiProviderType::Openai), // Qwen OAuth → Dashscope (OpenAI 兼容)
// API Key Provider 类型 - 直接映射
PoolProviderType::Anthropic => Some(ApiProviderType::Anthropic),
PoolProviderType::AzureOpenai => Some(ApiProviderType::AzureOpenai),
PoolProviderType::AwsBedrock => Some(ApiProviderType::AwsBedrock),
PoolProviderType::Ollama => Some(ApiProviderType::Ollama),
// OAuth-only,无降级
PoolProviderType::Kiro => None,
PoolProviderType::Codex => None,
PoolProviderType::ClaudeOAuth => None,
PoolProviderType::Antigravity => None,
PoolProviderType::IFlow => None,
}
}
/// 通过 ApiProviderType 查找凭证
fn find_by_api_type(
&self,
db: &DbConnection,
pool_type: &PoolProviderType,
api_type: &ApiProviderType,
) -> Result<Option<ProviderCredential>, String> {
let conn = db.lock().map_err(|e| e.to_string())?;
// 查找该类型的启用的 Provider(按 sort_order 排序)
let providers =
ApiKeyProviderDao::get_all_providers(&conn).map_err(|e| e.to_string())?;
let matching_providers: Vec<_> = providers
.into_iter()
.filter(|p| p.enabled && p.provider_type == *api_type)
.collect();
if matching_providers.is_empty() {
return Ok(None);
}
// 尝试从每个匹配的 Provider 获取可用的 API Key
for provider in matching_providers {
let keys = ApiKeyProviderDao::get_enabled_api_keys_by_provider(&conn, &provider.id)
.map_err(|e| e.to_string())?;
if keys.is_empty() {
continue;
}
// 轮询选择 API Key
let index = {
let mut indices = self.round_robin_index.write().map_err(|e| e.to_string())?;
indices
.entry(provider.id.clone())
.or_insert_with(|| AtomicUsize::new(0))
.fetch_add(1, Ordering::SeqCst)
};
let selected_key = &keys[index % keys.len()];
// 解密 API Key
let api_key = self.encryption.decrypt(&selected_key.api_key_encrypted)?;
// 转换为 ProviderCredential
let credential = self.convert_to_provider_credential(
pool_type,
api_type,
&provider,
&selected_key.id,
&api_key,
)?;
tracing::info!(
"[智能降级] 成功找到凭证: {:?} -> {} (key: {})",
pool_type,
provider.name,
selected_key.alias.as_deref().unwrap_or(&selected_key.id)
);
return Ok(Some(credential));
}
Ok(None)
}
/// 通过 provider_id 直接查找凭证 (支持 60+ Provider)
///
/// 例如: "deepseek", "dashscope", "openrouter"
fn find_by_provider_id(
&self,
db: &DbConnection,
provider_id: &str,
) -> Result<Option<ProviderCredential>, String> {
let conn = db.lock().map_err(|e| e.to_string())?;
// 直接按 provider_id 查找
let provider = ApiKeyProviderDao::get_provider_by_id(&conn, provider_id)
.map_err(|e| e.to_string())?;
let provider = match provider {
Some(p) if p.enabled => p,
_ => return Ok(None),
};
// 获取启用的 API Key
let keys = ApiKeyProviderDao::get_enabled_api_keys_by_provider(&conn, &provider.id)
.map_err(|e| e.to_string())?;
if keys.is_empty() {
return Ok(None);
}
// 轮询选择 API Key
let index = {
let mut indices = self.round_robin_index.write().map_err(|e| e.to_string())?;
indices
.entry(provider.id.clone())
.or_insert_with(|| AtomicUsize::new(0))
.fetch_add(1, Ordering::SeqCst)
};
let selected_key = &keys[index % keys.len()];
// 解密 API Key
let api_key = self.encryption.decrypt(&selected_key.api_key_encrypted)?;
// 转换为 OpenAI 兼容的 ProviderCredential
// 大多数 60+ Provider 都使用 OpenAI 兼容协议
let credential = self.convert_to_openai_compatible_credential(
&provider,
&selected_key.id,
&api_key,
)?;
tracing::info!(
"[智能降级] 成功通过 provider_id 找到凭证: {} (key: {})",
provider.name,
selected_key.alias.as_deref().unwrap_or(&selected_key.id)
);
Ok(Some(credential))
}
/// 转换为 ProviderCredential
fn convert_to_provider_credential(
&self,
pool_type: &PoolProviderType,
api_type: &ApiProviderType,
provider: &ApiKeyProvider,
key_id: &str,
api_key: &str,
) -> Result<ProviderCredential, String> {
let credential_data = match api_type {
ApiProviderType::Anthropic => CredentialData::ClaudeKey {
api_key: api_key.to_string(),
base_url: Some(provider.api_host.clone()),
},
ApiProviderType::Gemini => CredentialData::GeminiApiKey {
api_key: api_key.to_string(),
base_url: Some(provider.api_host.clone()),
excluded_models: Vec::new(),
},
ApiProviderType::Vertexai => CredentialData::VertexKey {
api_key: api_key.to_string(),
base_url: Some(provider.api_host.clone()),
model_aliases: std::collections::HashMap::new(),
},
// 其他类型(包括 Openai, OpenaiResponse 等)都用 OpenAI Key 格式
_ => CredentialData::OpenAIKey {
api_key: api_key.to_string(),
base_url: Some(provider.api_host.clone()),
},
};
let now = chrono::Utc::now();
Ok(ProviderCredential {
uuid: format!("fallback-{}", key_id),
provider_type: *pool_type,
credential: credential_data,
name: Some(format!("[降级] {}", provider.name)),
is_healthy: true,
is_disabled: false,
check_health: false, // 降级凭证不参与健康检查
check_model_name: None,
not_supported_models: Vec::new(),
usage_count: 0,
error_count: 0,
last_used: None,
last_error_time: None,
last_error_message: None,
last_health_check_time: None,
last_health_check_model: None,
created_at: now,
updated_at: now,
cached_token: None,
source: CredentialSource::Imported, // 标记为导入来源
proxy_url: None,
})
}
/// 转换为 OpenAI 兼容的 ProviderCredential
///
/// 用于 DeepSeek、Moonshot、智谱 等 60+ Provider
fn convert_to_openai_compatible_credential(
&self,
provider: &ApiKeyProvider,
key_id: &str,
api_key: &str,
) -> Result<ProviderCredential, String> {
let credential_data = CredentialData::OpenAIKey {
api_key: api_key.to_string(),
base_url: Some(provider.api_host.clone()), // 关键:使用 Provider 的 api_host
};
let now = chrono::Utc::now();
Ok(ProviderCredential {
uuid: format!("fallback-{}", key_id),
provider_type: PoolProviderType::OpenAI, // 统一使用 OpenAI 类型
credential: credential_data,
name: Some(format!("[降级] {}", provider.name)),
is_healthy: true,
is_disabled: false,
check_health: false, // 降级凭证不参与健康检查
check_model_name: None,
not_supported_models: Vec::new(),
usage_count: 0,
error_count: 0,
last_used: None,
last_error_time: None,
last_error_message: None,
last_health_check_time: None,
last_health_check_model: None,
created_at: now,
updated_at: now,
cached_token: None,
source: CredentialSource::Imported, // 标记为导入来源
proxy_url: None,
})
}
}
/// 导入结果
@@ -683,3 +994,7 @@ pub struct ImportResult {
}
use serde::{Deserialize, Serialize};
use crate::models::provider_pool_model::{
CredentialData, CredentialSource, PoolProviderType, ProviderCredential,
};
@@ -6,6 +6,7 @@
use crate::database::dao::provider_pool::ProviderPoolDao;
use crate::database::DbConnection;
use crate::services::api_key_provider_service::ApiKeyProviderService;
use crate::models::provider_pool_model::{
get_default_check_model, get_oauth_creds_path, CredentialData, CredentialDisplay,
HealthCheckResult, OAuthStatus, PoolProviderType, PoolStats, ProviderCredential,
@@ -316,6 +317,63 @@ impl ProviderPoolService {
Ok(Some(selected))
}
/// 带智能降级的凭证选择
///
/// 当 Provider Pool 无可用凭证时,自动从 API Key Provider 降级查找
///
/// # 参数
/// - `db`: 数据库连接
/// - `api_key_service`: API Key Provider 服务
/// - `provider_type`: Provider 类型字符串,如 "claude", "openai", "qwen"
/// - `model`: 可选的模型名称
/// - `provider_id_hint`: 可选的 provider_id 提示,用于 60+ Provider 直接查找
///
/// # 返回
/// - `Ok(Some(credential))`: 找到可用凭证(来自 Pool 或降级)
/// - `Ok(None)`: 没有找到任何可用凭证
/// - `Err(e)`: 查询过程中发生错误
pub fn select_credential_with_fallback(
&self,
db: &DbConnection,
api_key_service: &ApiKeyProviderService,
provider_type: &str,
model: Option<&str>,
provider_id_hint: Option<&str>,
) -> Result<Option<ProviderCredential>, String> {
// Step 1: 尝试从 Provider Pool 选择 (OAuth + API Key)
if let Some(cred) = self.select_credential(db, provider_type, model)? {
tracing::debug!(
"[凭证选择] 从 Provider Pool 找到 '{}' 凭证: {:?}",
provider_type,
cred.name
);
return Ok(Some(cred));
}
// Step 2: 智能降级到 API Key Provider
let pt: PoolProviderType = provider_type
.parse()
.unwrap_or(PoolProviderType::OpenAI);
// 传入 provider_id_hint 支持 60+ Provider
if let Some(cred) = api_key_service.get_fallback_credential(db, &pt, provider_id_hint)? {
tracing::info!(
"[智能降级] Provider Pool 无 '{}' 凭证,使用 API Key Provider 降级 (provider_id: {:?})",
provider_type,
provider_id_hint
);
return Ok(Some(cred));
}
// Step 3: 都没有找到
tracing::warn!(
"[凭证选择] 未找到 '{}' 的任何可用凭证 (provider_id_hint: {:?})",
provider_type,
provider_id_hint
);
Ok(None)
}
/// 基于权重分数选择最优凭证
fn select_best_credential_by_weight(
&self,