mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
co-authored by
Claude Opus 4.5
parent
373ea7da8c
commit
0a99125ba9
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})?
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user