mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: enable automatic decompression for HTTP responses
- Add gzip, brotli, deflate features to reqwest in Cargo.toml - Enable automatic decompression in ClaudeCustomProvider HTTP client - Enable automatic decompression in OpenAICustomProvider HTTP client - Add update_provider_env_vars command for Claude Code environment setup This fixes garbled response issue when upstream providers return compressed responses (e.g., anyrouter.top returning gzip-compressed data). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+35
@@ -144,6 +144,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.3"
|
||||
@@ -805,6 +817,24 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"compression-core",
|
||||
"flate2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -5993,13 +6023,18 @@ version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
|
||||
@@ -29,7 +29,7 @@ axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||
rustls-pemfile = "2"
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["limit", "cors"] }
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.12", features = ["json", "stream", "gzip", "brotli", "deflate"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
dirs = "5"
|
||||
|
||||
@@ -182,3 +182,158 @@ pub async fn set_endpoint_provider(
|
||||
);
|
||||
Ok(provider_display.to_string())
|
||||
}
|
||||
|
||||
/// 根据 API Key Provider 更新环境变量
|
||||
///
|
||||
/// 当用户在 API Server 页面选择一个 API Key Provider 时调用
|
||||
/// 会更新 ~/.claude/settings.json 和 shell 配置文件中的环境变量
|
||||
#[tauri::command]
|
||||
pub async fn update_provider_env_vars(
|
||||
logs: tauri::State<'_, LogState>,
|
||||
provider_type: String,
|
||||
api_host: String,
|
||||
api_key: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
use crate::services::live_sync::write_env_to_shell_config;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
|
||||
let home = dirs::home_dir().ok_or("Cannot find home directory")?;
|
||||
|
||||
// 根据 provider_type 确定要更新的环境变量
|
||||
// 参考 Claude Code 文档:https://code.claude.com/docs/en/llm-gateway
|
||||
let env_vars: Vec<(String, String)> = match provider_type.to_lowercase().as_str() {
|
||||
// Anthropic 兼容类型 - 包括大多数第三方 Provider
|
||||
// 如 DeepSeek、智谱、MiniMax、OpenRouter、AiHubMix 等
|
||||
"anthropic" | "new-api" | "gateway" => {
|
||||
let mut vars = vec![("ANTHROPIC_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("ANTHROPIC_AUTH_TOKEN".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
// OpenAI 兼容类型 - 用于 Codex 等
|
||||
"openai" | "openai-response" => {
|
||||
let mut vars = vec![("OPENAI_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("OPENAI_API_KEY".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
// Gemini 类型
|
||||
"gemini" => {
|
||||
let mut vars = vec![("GEMINI_API_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("GEMINI_API_KEY".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
// Azure OpenAI 类型
|
||||
"azure-openai" => {
|
||||
let mut vars = vec![("AZURE_OPENAI_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("AZURE_OPENAI_API_KEY".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
// Google Vertex AI 类型
|
||||
"vertexai" => {
|
||||
let mut vars = vec![("ANTHROPIC_VERTEX_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("GOOGLE_APPLICATION_CREDENTIALS".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
// AWS Bedrock 类型
|
||||
"aws-bedrock" => {
|
||||
let vars = vec![
|
||||
("ANTHROPIC_BEDROCK_BASE_URL".to_string(), api_host.clone()),
|
||||
("CLAUDE_CODE_USE_BEDROCK".to_string(), "1".to_string()),
|
||||
];
|
||||
// Bedrock 通常使用 AWS 凭证,不需要单独的 API Key
|
||||
vars
|
||||
}
|
||||
// Ollama 本地部署
|
||||
"ollama" => {
|
||||
let vars = vec![("OLLAMA_BASE_URL".to_string(), api_host.clone())];
|
||||
vars
|
||||
}
|
||||
_ => {
|
||||
// 未知类型,默认使用 ANTHROPIC_BASE_URL(因为大多数第三方 Provider 都是 Anthropic 兼容的)
|
||||
logs.write().await.add(
|
||||
"info",
|
||||
&format!(
|
||||
"Provider 类型 '{}' 使用默认 ANTHROPIC_BASE_URL",
|
||||
provider_type
|
||||
),
|
||||
);
|
||||
let mut vars = vec![("ANTHROPIC_BASE_URL".to_string(), api_host.clone())];
|
||||
if let Some(key) = api_key {
|
||||
vars.push(("ANTHROPIC_AUTH_TOKEN".to_string(), key));
|
||||
}
|
||||
vars
|
||||
}
|
||||
};
|
||||
|
||||
// 1. 更新 ~/.claude/settings.json
|
||||
let claude_dir = home.join(".claude");
|
||||
let settings_path = claude_dir.join("settings.json");
|
||||
|
||||
// 确保目录存在
|
||||
if !claude_dir.exists() {
|
||||
fs::create_dir_all(&claude_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// 读取现有配置
|
||||
let mut settings: Value = if settings_path.exists() {
|
||||
let content = fs::read_to_string(&settings_path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(&content).unwrap_or(json!({}))
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
|
||||
// 更新 env 字段
|
||||
let settings_obj = settings.as_object_mut().ok_or("Invalid settings format")?;
|
||||
if !settings_obj.contains_key("env") {
|
||||
settings_obj.insert("env".to_string(), json!({}));
|
||||
}
|
||||
|
||||
if let Some(env_obj) = settings_obj.get_mut("env").and_then(|v| v.as_object_mut()) {
|
||||
for (key, value) in &env_vars {
|
||||
env_obj.insert(key.clone(), json!(value));
|
||||
}
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
let content = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
|
||||
fs::write(&settings_path, content).map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. 更新 shell 配置文件
|
||||
if let Err(e) = write_env_to_shell_config(&env_vars) {
|
||||
logs.write()
|
||||
.await
|
||||
.add("warn", &format!("写入 shell 配置文件失败: {}", e));
|
||||
// 不中断流程
|
||||
}
|
||||
|
||||
logs.write().await.add(
|
||||
"info",
|
||||
&format!(
|
||||
"已更新 {} 环境变量: {}",
|
||||
provider_type,
|
||||
env_vars
|
||||
.iter()
|
||||
.map(|(k, _)| k.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"[CONFIG] Provider 环境变量已更新: type={}, api_host={}",
|
||||
provider_type,
|
||||
api_host
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -175,12 +175,40 @@ pub struct PluginUIInfo {
|
||||
/// 尝试读取 plugin.json 文件并解析为 PluginManifest
|
||||
fn read_plugin_manifest(install_path: &Path) -> Option<PluginManifest> {
|
||||
let manifest_path = install_path.join("plugin.json");
|
||||
tracing::debug!(
|
||||
"read_plugin_manifest: install_path={:?}, manifest_path={:?}, exists={}",
|
||||
install_path,
|
||||
manifest_path,
|
||||
manifest_path.exists()
|
||||
);
|
||||
|
||||
if !manifest_path.exists() {
|
||||
tracing::debug!("read_plugin_manifest: manifest file does not exist");
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&manifest_path).ok()?;
|
||||
serde_json::from_str(&content).ok()
|
||||
match std::fs::read_to_string(&manifest_path) {
|
||||
Ok(content) => {
|
||||
tracing::debug!(
|
||||
"read_plugin_manifest: file content length={}",
|
||||
content.len()
|
||||
);
|
||||
match serde_json::from_str(&content) {
|
||||
Ok(manifest) => {
|
||||
tracing::debug!("read_plugin_manifest: parsed successfully");
|
||||
Some(manifest)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("read_plugin_manifest: JSON parse error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("read_plugin_manifest: read file error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取带有 UI 配置的已安装插件列表
|
||||
@@ -322,7 +350,17 @@ pub async fn read_plugin_manifest_cmd(
|
||||
let plugins_dir = manager.plugins_dir();
|
||||
let plugin_path = plugins_dir.join(&plugin_id);
|
||||
|
||||
Ok(read_plugin_manifest(&plugin_path))
|
||||
tracing::debug!(
|
||||
"read_plugin_manifest_cmd: plugin_id={}, plugins_dir={:?}, plugin_path={:?}",
|
||||
plugin_id,
|
||||
plugins_dir,
|
||||
plugin_path
|
||||
);
|
||||
|
||||
let result = read_plugin_manifest(&plugin_path);
|
||||
tracing::debug!("read_plugin_manifest_cmd: result={:?}", result.is_some());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 启动插件 UI(用于 binary 类型插件)
|
||||
|
||||
@@ -29,6 +29,9 @@ fn create_http_client() -> Client {
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(600)) // 10 分钟总超时,支持长时间流式响应
|
||||
.tcp_keepalive(Duration::from_secs(60)) // TCP keepalive 保持连接活跃
|
||||
.gzip(true) // 自动解压 gzip 响应
|
||||
.brotli(true) // 自动解压 brotli 响应
|
||||
.deflate(true) // 自动解压 deflate 响应
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::models::openai::ChatCompletionRequest;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct OpenAICustomConfig {
|
||||
@@ -16,11 +17,24 @@ pub struct OpenAICustomProvider {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
/// 创建配置好的 HTTP 客户端
|
||||
fn create_http_client() -> Client {
|
||||
Client::builder()
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(600)) // 10 分钟总超时
|
||||
.tcp_keepalive(Duration::from_secs(60))
|
||||
.gzip(true) // 自动解压 gzip 响应
|
||||
.brotli(true) // 自动解压 brotli 响应
|
||||
.deflate(true) // 自动解压 deflate 响应
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new())
|
||||
}
|
||||
|
||||
impl Default for OpenAICustomProvider {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: OpenAICustomConfig::default(),
|
||||
client: Client::new(),
|
||||
client: create_http_client(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +52,7 @@ impl OpenAICustomProvider {
|
||||
base_url,
|
||||
enabled: true,
|
||||
},
|
||||
client: Client::new(),
|
||||
client: create_http_client(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user