feat: 添加 X-Provider-Id header 支持实现精确 Provider 路由

- 在 API 请求中添加 X-Provider-Id header 用于精确路由到指定 Provider
- 修复 Chat 专家模式使用 Deepseek 等自定义 Provider 时路由错误的问题
- 优化 toast 通知持续时间和去重机制
- 移除 App.tsx 中重复的 Toaster 组件

修改文件:
- api.rs: 解析 X-Provider-Id header 并支持 provider_id_hint 路由
- api_key_provider_service.rs: 添加 get_next_api_key_with_provider_info 方法
- protocols/mod.rs, openai.rs, anthropic.rs: Protocol trait 添加 provider_id 参数
- native_agent.rs, native_agent_cmd.rs: 传递 provider_id 到请求中
- useAgentChat.ts: 增加错误 toast 持续时间到 8 秒
- sonner.tsx: 默认 toast 持续时间从 3 秒增加到 5 秒

🤖 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-06 23:55:58 +08:00
committed by coso
co-authored by Claude Opus 4.5
parent 735304a941
commit 3a740969b9
6 changed files with 104 additions and 21 deletions
+26 -8
View File
@@ -375,12 +375,14 @@ impl Protocol for AnthropicProtocol {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String> {
info!(
"[AnthropicProtocol] 发送流式请求: model={}, history_len={}, tools_count={}",
"[AnthropicProtocol] 发送流式请求: model={}, history_len={}, tools_count={}, provider_id={:?}",
model,
messages.len(),
tools.map(|t| t.len()).unwrap_or(0)
tools.map(|t| t.len()).unwrap_or(0),
provider_id
);
let (anthropic_messages, system) =
@@ -400,11 +402,18 @@ impl Protocol for AnthropicProtocol {
let url = format!("{}{}", base_url, self.endpoint());
let response = client
let mut req_builder = client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.header("anthropic-version", "2023-06-01");
// 添加 X-Provider-Id header 用于精确路由
if let Some(pid) = provider_id {
req_builder = req_builder.header("X-Provider-Id", pid);
}
let response = req_builder
.json(&request)
.send()
.await
@@ -435,12 +444,14 @@ impl Protocol for AnthropicProtocol {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String> {
debug!(
"[AnthropicProtocol] 继续流式对话: model={}, history_len={}, tools_count={}",
"[AnthropicProtocol] 继续流式对话: model={}, history_len={}, tools_count={}, provider_id={:?}",
model,
messages.len(),
tools.map(|t| t.len()).unwrap_or(0)
tools.map(|t| t.len()).unwrap_or(0),
provider_id
);
let (anthropic_messages, system) = Self::build_messages_from_history(messages, config);
@@ -459,11 +470,18 @@ impl Protocol for AnthropicProtocol {
let url = format!("{}{}", base_url, self.endpoint());
let response = client
let mut req_builder = client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.header("anthropic-version", "2023-06-01");
// 添加 X-Provider-Id header 用于精确路由
if let Some(pid) = provider_id {
req_builder = req_builder.header("X-Provider-Id", pid);
}
let response = req_builder
.json(&request)
.send()
.await
+2
View File
@@ -36,6 +36,7 @@ pub trait Protocol: Send + Sync {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String>;
/// 继续流式对话(工具调用后)
@@ -51,6 +52,7 @@ pub trait Protocol: Send + Sync {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String>;
/// 获取 API 端点
+28 -10
View File
@@ -358,12 +358,14 @@ impl Protocol for OpenAIProtocol {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String> {
info!(
"[OpenAIProtocol] 发送流式请求: model={}, history_len={}, tools_count={}",
"[OpenAIProtocol] 发送流式请求: model={}, history_len={}, tools_count={}, provider_id={:?}",
model,
messages.len(),
tools.map(|t| t.len()).unwrap_or(0)
tools.map(|t| t.len()).unwrap_or(0),
provider_id
);
let chat_messages = Self::build_messages(messages, user_message, images, config);
@@ -387,14 +389,21 @@ impl Protocol for OpenAIProtocol {
let url = format!("{}{}", base_url, self.endpoint());
eprintln!(
"[OpenAIProtocol] 发送请求到: {} model={} stream={}",
url, model, request.stream
"[OpenAIProtocol] 发送请求到: {} model={} stream={} provider_id={:?}",
url, model, request.stream, provider_id
);
let response = client
let mut req_builder = client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Content-Type", "application/json");
// 添加 X-Provider-Id header 用于精确路由
if let Some(pid) = provider_id {
req_builder = req_builder.header("X-Provider-Id", pid);
}
let response = req_builder
.json(&request)
.send()
.await
@@ -429,12 +438,14 @@ impl Protocol for OpenAIProtocol {
config: &AgentConfig,
tools: Option<&[Tool]>,
tx: mpsc::Sender<StreamEvent>,
provider_id: Option<&str>,
) -> Result<StreamResult, String> {
debug!(
"[OpenAIProtocol] 继续流式对话: model={}, history_len={}, tools_count={}",
"[OpenAIProtocol] 继续流式对话: model={}, history_len={}, tools_count={}, provider_id={:?}",
model,
messages.len(),
tools.map(|t| t.len()).unwrap_or(0)
tools.map(|t| t.len()).unwrap_or(0),
provider_id
);
let chat_messages = Self::build_messages_from_history(messages, config);
@@ -457,10 +468,17 @@ impl Protocol for OpenAIProtocol {
let url = format!("{}{}", base_url, self.endpoint());
let response = client
let mut req_builder = client
.post(&url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Content-Type", "application/json");
// 添加 X-Provider-Id header 用于精确路由
if let Some(pid) = provider_id {
req_builder = req_builder.header("X-Provider-Id", pid);
}
let response = req_builder
.json(&request)
.send()
.await
@@ -492,6 +492,53 @@ impl ApiKeyProviderService {
.map_err(|e| e.to_string())
}
/// 获取下一个可用的 API Key 以及 Provider 信息(按 provider_id 精确查找)
/// 用于支持 X-Provider-Id 请求头指定具体的 Provider
pub fn get_next_api_key_with_provider_info(
&self,
db: &DbConnection,
provider_id: &str,
) -> Result<Option<(String, ApiKeyProvider)>, String> {
let conn = db.lock().map_err(|e| e.to_string())?;
// 获取 Provider 信息
let provider = match ApiKeyProviderDao::get_provider_by_id(&conn, provider_id)
.map_err(|e| e.to_string())?
{
Some(p) => p,
None => return Ok(None),
};
// 检查 Provider 是否启用
if !provider.enabled {
return Ok(None);
}
// 获取该 Provider 的所有启用的 API Keys
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);
}
// 获取或创建轮询索引
let index = {
let mut indices = self.round_robin_index.write().map_err(|e| e.to_string())?;
indices
.entry(provider_id.to_string())
.or_insert_with(|| AtomicUsize::new(0))
.fetch_add(1, Ordering::SeqCst)
};
// 选择 API Key
let selected_key = &keys[index % keys.len()];
// 解密并返回
let decrypted = self.encryption.decrypt(&selected_key.api_key_encrypted)?;
Ok(Some((decrypted, provider)))
}
/// 按 Provider 类型获取下一个可用的 API Key(轮询负载均衡)
/// 这个方法会查找所有该类型的 Provider(包括自定义 Provider)
pub fn get_next_api_key_by_type(
-2
View File
@@ -19,7 +19,6 @@ import { ToolsPage } from "./components/tools/ToolsPage";
import { AgentChatPage } from "./components/agent";
import { PluginUIRenderer } from "./components/plugins/PluginUIRenderer";
import { PluginsPage } from "./components/plugins/PluginsPage";
import { Toaster } from "./components/ui/sonner";
import { flowEventManager } from "./lib/flowEventManager";
import { OnboardingWizard, useOnboardingState } from "./components/onboarding";
import { ConnectConfirmDialog } from "./components/connect";
@@ -209,7 +208,6 @@ function App() {
<AppContainer>
<AppSidebar currentPage={currentPage} onNavigate={setCurrentPage} />
<MainContent>{renderPage()}</MainContent>
<Toaster />
{/* ProxyCast Connect 确认弹窗 */}
{/* _Requirements: 5.2_ */}
<ConnectConfirmDialog
+1 -1
View File
@@ -7,7 +7,7 @@ export function Toaster() {
richColors
theme="system"
toastOptions={{
duration: 3000,
duration: 5000,
classNames: {
toast:
"group rounded-md border bg-background text-foreground shadow-lg",