mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: add OpenAI and Claude custom providers
- Add OpenAICustomProvider for custom OpenAI-compatible APIs - Add ClaudeCustomProvider for custom Claude APIs - Add configuration UI with API key and base URL inputs - Update Providers page with all 5 provider tabs - All providers now manageable from single page
This commit is contained in:
@@ -533,6 +533,78 @@ async fn check_and_reload_qwen_credentials(
|
||||
}
|
||||
}
|
||||
|
||||
// ============ OpenAI Custom Provider Commands ============
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct OpenAICustomStatus {
|
||||
enabled: bool,
|
||||
has_api_key: bool,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_openai_custom_status(state: tauri::State<'_, AppState>) -> Result<OpenAICustomStatus, String> {
|
||||
let s = state.read().await;
|
||||
let config = &s.openai_custom_provider.config;
|
||||
Ok(OpenAICustomStatus {
|
||||
enabled: config.enabled,
|
||||
has_api_key: config.api_key.is_some(),
|
||||
base_url: s.openai_custom_provider.get_base_url(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_openai_custom_config(
|
||||
state: tauri::State<'_, AppState>,
|
||||
logs: tauri::State<'_, LogState>,
|
||||
api_key: Option<String>,
|
||||
base_url: Option<String>,
|
||||
enabled: bool,
|
||||
) -> Result<String, String> {
|
||||
let mut s = state.write().await;
|
||||
s.openai_custom_provider.config.api_key = api_key;
|
||||
s.openai_custom_provider.config.base_url = base_url;
|
||||
s.openai_custom_provider.config.enabled = enabled;
|
||||
logs.write().await.add("info", &format!("[OpenAI Custom] 配置已更新, enabled={}", enabled));
|
||||
Ok("OpenAI Custom config updated".to_string())
|
||||
}
|
||||
|
||||
// ============ Claude Custom Provider Commands ============
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct ClaudeCustomStatus {
|
||||
enabled: bool,
|
||||
has_api_key: bool,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_claude_custom_status(state: tauri::State<'_, AppState>) -> Result<ClaudeCustomStatus, String> {
|
||||
let s = state.read().await;
|
||||
let config = &s.claude_custom_provider.config;
|
||||
Ok(ClaudeCustomStatus {
|
||||
enabled: config.enabled,
|
||||
has_api_key: config.api_key.is_some(),
|
||||
base_url: s.claude_custom_provider.get_base_url(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_claude_custom_config(
|
||||
state: tauri::State<'_, AppState>,
|
||||
logs: tauri::State<'_, LogState>,
|
||||
api_key: Option<String>,
|
||||
base_url: Option<String>,
|
||||
enabled: bool,
|
||||
) -> Result<String, String> {
|
||||
let mut s = state.write().await;
|
||||
s.claude_custom_provider.config.api_key = api_key;
|
||||
s.claude_custom_provider.config.base_url = base_url;
|
||||
s.claude_custom_provider.config.enabled = enabled;
|
||||
logs.write().await.add("info", &format!("[Claude Custom] 配置已更新, enabled={}", enabled));
|
||||
Ok("Claude Custom config updated".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_logs(logs: tauri::State<'_, LogState>) -> Result<Vec<logger::LogEntry>, String> {
|
||||
Ok(logs.read().await.get_logs())
|
||||
@@ -638,6 +710,12 @@ pub fn run() {
|
||||
get_qwen_env_variables,
|
||||
get_qwen_token_file_hash,
|
||||
check_and_reload_qwen_credentials,
|
||||
// OpenAI Custom commands
|
||||
get_openai_custom_status,
|
||||
set_openai_custom_config,
|
||||
// Claude Custom commands
|
||||
get_claude_custom_status,
|
||||
set_claude_custom_config,
|
||||
// Common
|
||||
get_logs,
|
||||
clear_logs,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Claude Custom Provider (自定义 Claude API)
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ClaudeCustomConfig {
|
||||
pub api_key: Option<String>,
|
||||
pub base_url: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct ClaudeCustomProvider {
|
||||
pub config: ClaudeCustomConfig,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl ClaudeCustomProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: ClaudeCustomConfig::default(),
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base_url(&self) -> String {
|
||||
self.config.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://api.anthropic.com".to_string())
|
||||
}
|
||||
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.config.api_key.is_some() && self.config.enabled
|
||||
}
|
||||
|
||||
pub async fn messages(
|
||||
&self,
|
||||
request: &serde_json::Value,
|
||||
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
|
||||
let api_key = self.config.api_key.as_ref()
|
||||
.ok_or("Claude API key not configured")?;
|
||||
|
||||
let base_url = self.get_base_url();
|
||||
let url = format!("{}/v1/messages", base_url);
|
||||
|
||||
let resp = self.client
|
||||
.post(&url)
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn count_tokens(
|
||||
&self,
|
||||
request: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
|
||||
let api_key = self.config.api_key.as_ref()
|
||||
.ok_or("Claude API key not configured")?;
|
||||
|
||||
let base_url = self.get_base_url();
|
||||
let url = format!("{}/v1/messages/count_tokens", base_url);
|
||||
|
||||
let resp = self.client
|
||||
.post(&url)
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("Failed to count tokens: {} - {}", status, body).into());
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
pub mod kiro;
|
||||
pub mod gemini;
|
||||
pub mod qwen;
|
||||
pub mod openai_custom;
|
||||
pub mod claude_custom;
|
||||
|
||||
pub use kiro::KiroProvider;
|
||||
pub use gemini::GeminiProvider;
|
||||
pub use qwen::QwenProvider;
|
||||
pub use openai_custom::OpenAICustomProvider;
|
||||
pub use claude_custom::ClaudeCustomProvider;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//! OpenAI Custom Provider (自定义 OpenAI 兼容 API)
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct OpenAICustomConfig {
|
||||
pub api_key: Option<String>,
|
||||
pub base_url: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct OpenAICustomProvider {
|
||||
pub config: OpenAICustomConfig,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl OpenAICustomProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: OpenAICustomConfig::default(),
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_base_url(&self) -> String {
|
||||
self.config.base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://api.openai.com/v1".to_string())
|
||||
}
|
||||
|
||||
pub fn is_configured(&self) -> bool {
|
||||
self.config.api_key.is_some() && self.config.enabled
|
||||
}
|
||||
|
||||
pub async fn chat_completions(
|
||||
&self,
|
||||
request: &serde_json::Value,
|
||||
) -> Result<reqwest::Response, Box<dyn Error + Send + Sync>> {
|
||||
let api_key = self.config.api_key.as_ref()
|
||||
.ok_or("OpenAI API key not configured")?;
|
||||
|
||||
let base_url = self.get_base_url();
|
||||
let url = format!("{}/chat/completions", base_url);
|
||||
|
||||
let resp = self.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn list_models(&self) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
|
||||
let api_key = self.config.api_key.as_ref()
|
||||
.ok_or("OpenAI API key not configured")?;
|
||||
|
||||
let base_url = self.get_base_url();
|
||||
let url = format!("{}/models", base_url);
|
||||
|
||||
let resp = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(format!("Failed to list models: {} - {}", status, body).into());
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ use crate::converter::anthropic_to_openai::convert_anthropic_to_openai;
|
||||
use crate::providers::kiro::KiroProvider;
|
||||
use crate::providers::gemini::GeminiProvider;
|
||||
use crate::providers::qwen::QwenProvider;
|
||||
use crate::providers::openai_custom::OpenAICustomProvider;
|
||||
use crate::providers::claude_custom::ClaudeCustomProvider;
|
||||
use crate::logger::LogStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -35,6 +37,8 @@ pub struct ServerState {
|
||||
pub kiro_provider: KiroProvider,
|
||||
pub gemini_provider: GeminiProvider,
|
||||
pub qwen_provider: QwenProvider,
|
||||
pub openai_custom_provider: OpenAICustomProvider,
|
||||
pub claude_custom_provider: ClaudeCustomProvider,
|
||||
shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
@@ -49,6 +53,9 @@ impl ServerState {
|
||||
let mut qwen = QwenProvider::new();
|
||||
let _ = qwen.load_credentials();
|
||||
|
||||
let openai_custom = OpenAICustomProvider::new();
|
||||
let claude_custom = ClaudeCustomProvider::new();
|
||||
|
||||
Self {
|
||||
config,
|
||||
running: false,
|
||||
@@ -57,6 +64,8 @@ impl ServerState {
|
||||
kiro_provider: kiro,
|
||||
gemini_provider: gemini,
|
||||
qwen_provider: qwen,
|
||||
openai_custom_provider: openai_custom,
|
||||
claude_custom_provider: claude_custom,
|
||||
shutdown_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,3 +217,52 @@ export async function getQwenTokenFileHash(): Promise<string> {
|
||||
export async function checkAndReloadQwenCredentials(lastHash: string): Promise<CheckResult> {
|
||||
return invoke("check_and_reload_qwen_credentials", { last_hash: lastHash });
|
||||
}
|
||||
|
||||
|
||||
// ============ OpenAI Custom Provider ============
|
||||
|
||||
export interface OpenAICustomStatus {
|
||||
enabled: boolean;
|
||||
has_api_key: boolean;
|
||||
base_url: string;
|
||||
}
|
||||
|
||||
export async function getOpenAICustomStatus(): Promise<OpenAICustomStatus> {
|
||||
return invoke("get_openai_custom_status");
|
||||
}
|
||||
|
||||
export async function setOpenAICustomConfig(
|
||||
apiKey: string | null,
|
||||
baseUrl: string | null,
|
||||
enabled: boolean
|
||||
): Promise<string> {
|
||||
return invoke("set_openai_custom_config", {
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
enabled
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Claude Custom Provider ============
|
||||
|
||||
export interface ClaudeCustomStatus {
|
||||
enabled: boolean;
|
||||
has_api_key: boolean;
|
||||
base_url: string;
|
||||
}
|
||||
|
||||
export async function getClaudeCustomStatus(): Promise<ClaudeCustomStatus> {
|
||||
return invoke("get_claude_custom_status");
|
||||
}
|
||||
|
||||
export async function setClaudeCustomConfig(
|
||||
apiKey: string | null,
|
||||
baseUrl: string | null,
|
||||
enabled: boolean
|
||||
): Promise<string> {
|
||||
return invoke("set_claude_custom_config", {
|
||||
api_key: apiKey,
|
||||
base_url: baseUrl,
|
||||
enabled
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user