Compare commits

...

22 Commits

Author SHA1 Message Date
YoVinchen 543d0df5c1 Merge branch 'main' into feat/provider-chat-completions 2026-01-22 11:08:20 +08:00
YoVinchen 6d7ec14644 Merge branch 'main' into feat/provider-chat-completions 2026-01-20 23:57:24 +08:00
YoVinchen 8f1ad6e057 feat: improve app visibility settings and keyboard shortcuts
- Add ESC key navigation: return to previous view from full-screen panels
- Sync tray menu with app visibility settings (hide disabled apps)
- Add fnm (fast node manager) path support for CLI version scanning
- Fix useModelState being overwritten when user is editing model fields
- Fix EditProviderDialog showing stale data after save and reopen
- Refactor AppSwitcher to use loop instead of repetitive buttons
- Extract ToggleRow as reusable UI component
- Add domUtils for checking text-editable elements
2026-01-20 21:53:47 +08:00
YoVinchen 2b3c80703c Merge branch 'main' into feat/provider-chat-completions
# Conflicts:
#	src-tauri/src/lib.rs
#	src/types.ts
2026-01-20 21:39:14 +08:00
YoVinchen eef328d2a4 feat(proxy): add ChatCompletions compatibility mode for Claude adapter
- Add ChatCompletions provider type for generic OpenAI-compatible endpoints
- Implement chat_completions_mode detection in Claude adapter
- Fix endpoint duplication when base_url already contains chat/completions
- Improve Anthropic SSE format conversion with complete message_start event
- Add ChatCompletions preset configuration
2026-01-20 17:57:08 +08:00
YoVinchen 4eb983c58f feat(proxy): add request logging for debugging
Add debug logs for outgoing requests including URL and body content
with byte size, matching the existing response logging format.
2026-01-20 16:59:27 +08:00
YoVinchen cef3812745 fix(stream-check): use Gemini native streaming API format
- Change endpoint from OpenAI-compatible to native streamGenerateContent
- Add alt=sse parameter for SSE format response
- Use x-goog-api-key header instead of Bearer token
- Convert request body to Gemini contents/parts format
2026-01-20 14:39:12 +08:00
YoVinchen 6c9a7ef949 fix(provider): allow typing :// in endpoint URL inputs
Change input type from "url" to "text" to prevent browser
URL validation from blocking :// input.

Closes #681
2026-01-20 14:02:38 +08:00
YoVinchen 48c434a20a fix(tray): fix clippy uninlined format args warning
Use inline format arguments: {app_type_str} instead of {}
2026-01-20 11:37:39 +08:00
YoVinchen 4f6dfff179 fix(log): enable dynamic log level and single file mode
- Initialize log at Trace level for dynamic adjustment
- Change rotation strategy to KeepSome(1) for single file
- Set max file size to 1GB
- Delete old log file on startup for clean start
2026-01-20 10:41:22 +08:00
YoVinchen 4b9cca12d3 fix(tray): restore tray-provider events and enable Auto failover properly
- Emit provider-switched event on tray provider click (backward compatibility)
- Auto button now: starts proxy, takes over live config, enables failover
2026-01-19 23:36:06 +08:00
YoVinchen 8db44a78b2 fix(i18n): add providerAdvanced i18n keys and fix failover toast parameter
- Add providerAdvanced.* i18n keys to en.json, zh.json, and ja.json
- Fix failover toggleFailed toast to pass detail parameter
- Remove Chinese fallback text from UI for English/Japanese users
2026-01-19 22:51:50 +08:00
YoVinchen efad0c0f91 fix(proxy): prevent proxy recursion when system proxy points to localhost
Detect if HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY environment variables
point to loopback addresses (localhost, 127.0.0.1), and bypass system
proxy in such cases to avoid infinite request loops.
2026-01-19 22:28:55 +08:00
YoVinchen b536bd0366 fix(proxy): filter x-goog-api-key header to prevent duplication 2026-01-19 21:13:07 +08:00
YoVinchen 783bc60329 docs(proxy): update timeout config descriptions and defaults
Fixes #612
2026-01-19 21:13:07 +08:00
YoVinchen 924e5386f9 fix(proxy): increase request body size limit to 200MB
Fixes #666
2026-01-19 21:13:07 +08:00
YoVinchen 74f67bc1ee feat(settings): add log config management
Fixes #612
Fixes #514
2026-01-19 21:13:07 +08:00
YoVinchen ae5d05b08c fix(ui): sync toast theme with app setting 2026-01-19 21:13:07 +08:00
YoVinchen 4edb08cd53 fix(proxy): support system proxy fallback and provider-level proxy config
- Remove no_proxy() calls in http_client.rs to allow system proxy fallback
- Add get_for_provider() to build HTTP client with provider-specific proxy
- Update forwarder.rs and stream_check.rs to use provider proxy config
- Fix EditProviderDialog.tsx to include provider.meta in useMemo deps
- Add useEffect in ProviderAdvancedConfig.tsx to sync expand state

Fixes #636
Fixes #583
2026-01-19 21:12:52 +08:00
YoVinchen a8ea99c3fe feat(ui): add failover toggle and improve proxy controls
- Add FailoverToggle component with slide animation
- Simplify ProxyToggle style to match FailoverToggle
- Add usage statistics button when proxy is active
- Fix i18n parameter passing for failover messages
- Add missing failover translation keys (inQueue, addQueue, priority)
- Replace AboutSection icon with app logo
2026-01-19 21:12:52 +08:00
YoVinchen 22c0e7bb5c feat(provider): add individual test and proxy config for providers
Add support for provider-specific model test and proxy configurations:

- Add ProviderTestConfig and ProviderProxyConfig types in Rust and TypeScript
- Create ProviderAdvancedConfig component with collapsible panels
- Update stream_check service to merge provider config with global config
- Proxy config UI follows global proxy style (single URL input)

Provider-level configs stored in meta field, no database schema changes needed.
2026-01-19 21:12:20 +08:00
YoVinchen 8af31c3e61 refactor(ui): simplify UpdateBadge to minimal dot indicator 2026-01-19 21:12:20 +08:00
5 changed files with 71 additions and 21 deletions
+10 -3
View File
@@ -561,12 +561,19 @@ impl RequestForwarder {
// 检查是否需要格式转换
let needs_transform = adapter.needs_transform(provider);
let effective_endpoint =
if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" {
// 确定有效端点:
// - 如果需要转换且是 Claude 的 /v1/messages 端点,改写为 /v1/chat/completions
// - 但如果 base_url 已包含 chat/completions,则不再自动添加
let effective_endpoint = if needs_transform && adapter.name() == "Claude" {
let base_has_chat_completions = base_url.contains("chat/completions");
if endpoint == "/v1/messages" && !base_has_chat_completions {
"/v1/chat/completions"
} else {
endpoint
};
}
} else {
endpoint
};
// 使用适配器构建 URL
let url = adapter.build_url(&base_url, effective_endpoint);
+23 -7
View File
@@ -23,10 +23,16 @@ impl ClaudeAdapter {
/// 获取供应商类型
///
/// 根据 base_url 和 auth_mode 检测具体的供应商类型:
/// - ChatCompletions: chat_completions_mode 为 true(优先级最高)
/// - OpenRouter: base_url 包含 openrouter.ai
/// - ClaudeAuth: auth_mode 为 bearer_only
/// - Claude: 默认 Anthropic 官方
pub fn provider_type(&self, provider: &Provider) -> ProviderType {
// 检测 ChatCompletions 模式(优先级最高)
if self.is_chat_completions_mode(provider) {
return ProviderType::ChatCompletions;
}
// 检测 OpenRouter
if self.is_openrouter(provider) {
return ProviderType::OpenRouter;
@@ -48,6 +54,20 @@ impl ClaudeAdapter {
false
}
/// 检测是否启用 ChatCompletions 兼容模式
fn is_chat_completions_mode(&self, provider: &Provider) -> bool {
let raw = provider.settings_config.get("chat_completions_mode");
match raw {
Some(serde_json::Value::Bool(enabled)) => *enabled,
Some(serde_json::Value::Number(num)) => num.as_i64().unwrap_or(0) != 0,
Some(serde_json::Value::String(value)) => {
let normalized = value.trim().to_lowercase();
normalized == "true" || normalized == "1"
}
_ => false,
}
}
/// 检测 OpenRouter 是否启用兼容模式
fn is_openrouter_compat_enabled(&self, provider: &Provider) -> bool {
if !self.is_openrouter(provider) {
@@ -253,13 +273,9 @@ impl ProviderAdapter for ClaudeAdapter {
}
}
fn needs_transform(&self, _provider: &Provider) -> bool {
// NOTE:
// OpenRouter 已推出 Claude Code 兼容接口(可直接处理 `/v1/messages`),默认不再启用
// Anthropic ↔ OpenAI 的格式转换。
//
// 如果未来需要回退到旧的 OpenAI Chat Completions 方案,可恢复下面这行:
self.is_openrouter_compat_enabled(_provider)
fn needs_transform(&self, provider: &Provider) -> bool {
// ChatCompletions 模式或 OpenRouter 兼容模式都需要转换
self.is_chat_completions_mode(provider) || self.is_openrouter_compat_enabled(provider)
}
fn transform_request(
+11 -3
View File
@@ -50,6 +50,8 @@ pub enum ProviderType {
GeminiCli,
/// OpenRouter(已支持 Claude Code 兼容接口,默认透传;保留旧转换逻辑备用)
OpenRouter,
/// ChatCompletions 兼容模式(Anthropic ↔ OpenAI 格式转换,不限制上游地址)
ChatCompletions,
}
impl ProviderType {
@@ -75,6 +77,7 @@ impl ProviderType {
"https://generativelanguage.googleapis.com"
}
ProviderType::OpenRouter => "https://openrouter.ai/api",
ProviderType::ChatCompletions => "https://api.openai.com",
}
}
@@ -148,6 +151,7 @@ impl ProviderType {
ProviderType::Gemini => "gemini",
ProviderType::GeminiCli => "gemini_cli",
ProviderType::OpenRouter => "openrouter",
ProviderType::ChatCompletions => "chat_completions",
}
}
}
@@ -169,6 +173,9 @@ impl std::str::FromStr for ProviderType {
"gemini" => Ok(ProviderType::Gemini),
"gemini_cli" | "gemini-cli" => Ok(ProviderType::GeminiCli),
"openrouter" => Ok(ProviderType::OpenRouter),
"chat_completions" | "chat-completions" | "chatcompletions" => {
Ok(ProviderType::ChatCompletions)
}
_ => Err(format!("Invalid provider type: {s}")),
}
}
@@ -191,9 +198,10 @@ pub fn get_adapter(app_type: &AppType) -> Box<dyn ProviderAdapter> {
#[allow(dead_code)]
pub fn get_adapter_for_provider_type(provider_type: &ProviderType) -> Box<dyn ProviderAdapter> {
match provider_type {
ProviderType::Claude | ProviderType::ClaudeAuth | ProviderType::OpenRouter => {
Box::new(ClaudeAdapter::new())
}
ProviderType::Claude
| ProviderType::ClaudeAuth
| ProviderType::OpenRouter
| ProviderType::ChatCompletions => Box::new(ClaudeAdapter::new()),
ProviderType::Codex => Box::new(CodexAdapter::new()),
ProviderType::Gemini | ProviderType::GeminiCli => Box::new(GeminiAdapter::new()),
}
+13 -8
View File
@@ -115,16 +115,22 @@ pub fn create_anthropic_sse_stream(
if let Some(choice) = chunk.choices.first() {
if !has_sent_message_start {
// 构建完整的 message_start 事件,与原生 Anthropic 格式一致
let event = json!({
"type": "message_start",
"message": {
"id": message_id.clone().unwrap_or_default(),
"type": "message",
"role": "assistant",
"content": [],
"model": current_model.clone().unwrap_or_default(),
"stop_reason": null,
"stop_sequence": null,
"usage": {
"input_tokens": 0,
"output_tokens": 0
"input_tokens": chunk.usage.as_ref().map(|u| u.prompt_tokens).unwrap_or(1),
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
});
@@ -272,18 +278,17 @@ pub fn create_anthropic_sse_stream(
}
let stop_reason = map_stop_reason(Some(finish_reason));
// 构建 usage 信息,包含 input_tokens 和 output_tokens
let usage_json = chunk.usage.as_ref().map(|u| json!({
"input_tokens": u.prompt_tokens,
"output_tokens": u.completion_tokens
}));
// 构建 usage 信息,Anthropic 格式只需要 output_tokens
let output_tokens = chunk.usage.as_ref().map(|u| u.completion_tokens).unwrap_or(0);
let event = json!({
"type": "message_delta",
"delta": {
"stop_reason": stop_reason,
"stop_sequence": null
},
"usage": usage_json
"usage": {
"output_tokens": output_tokens
}
});
let sse_data = format!("event: message_delta\ndata: {}\n\n",
serde_json::to_string(&event).unwrap_or_default());
+14
View File
@@ -479,6 +479,20 @@ export const providerPresets: ProviderPreset[] = [
icon: "openrouter",
iconColor: "#6566F1",
},
{
name: "ChatCompletions",
websiteUrl: "",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "",
ANTHROPIC_AUTH_TOKEN: "",
},
chat_completions_mode: true,
},
category: "third_party",
icon: "openai",
iconColor: "#10A37F",
},
{
name: "Xiaomi MiMo",
websiteUrl: "https://platform.xiaomimimo.com",