merge: resolve conflicts between fix/common-config-sync-improvements and main

Merged both feature sets:
- common-config: commonConfigEnabled, commonConfigEnabledByApp fields
- api-format: apiFormat field for Claude provider OpenAI compatibility

Key resolution in live.rs: apply sanitize_claude_settings_for_live()
to merged config before writing to ensure internal fields are stripped.
This commit is contained in:
YoVinchen
2026-01-31 21:03:08 +08:00
36 changed files with 892 additions and 402 deletions
+25 -15
View File
@@ -20,6 +20,8 @@ jobs:
include: include:
- os: windows-2022 - os: windows-2022
- os: ubuntu-22.04 - os: ubuntu-22.04
- os: ubuntu-22.04-arm
arch: arm64
- os: macos-14 - os: macos-14
steps: steps:
@@ -85,8 +87,8 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ${{ steps.pnpm-store.outputs.path }} path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-store- restore-keys: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-
- name: Install frontend deps - name: Install frontend deps
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -256,10 +258,11 @@ jobs:
set -euxo pipefail set -euxo pipefail
mkdir -p release-assets mkdir -p release-assets
VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0 VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0
ARCH="${{ matrix.arch || 'x86_64' }}"
# Updater artifact: AppImage(含对应 .sig # Updater artifact: AppImage(含对应 .sig
APPIMAGE=$(find src-tauri/target/release/bundle -name "*.AppImage" | head -1 || true) APPIMAGE=$(find src-tauri/target/release/bundle -name "*.AppImage" | head -1 || true)
if [ -n "$APPIMAGE" ]; then if [ -n "$APPIMAGE" ]; then
NEW_APPIMAGE="CC-Switch-${VERSION}-Linux.AppImage" NEW_APPIMAGE="CC-Switch-${VERSION}-Linux-${ARCH}.AppImage"
cp "$APPIMAGE" "release-assets/$NEW_APPIMAGE" cp "$APPIMAGE" "release-assets/$NEW_APPIMAGE"
[ -f "$APPIMAGE.sig" ] && cp "$APPIMAGE.sig" "release-assets/$NEW_APPIMAGE.sig" || echo ".sig for AppImage not found" [ -f "$APPIMAGE.sig" ] && cp "$APPIMAGE.sig" "release-assets/$NEW_APPIMAGE.sig" || echo ".sig for AppImage not found"
echo "AppImage copied: $NEW_APPIMAGE" echo "AppImage copied: $NEW_APPIMAGE"
@@ -269,18 +272,16 @@ jobs:
# 额外上传 .deb(用于手动安装,不参与 Updater) # 额外上传 .deb(用于手动安装,不参与 Updater)
DEB=$(find src-tauri/target/release/bundle -name "*.deb" | head -1 || true) DEB=$(find src-tauri/target/release/bundle -name "*.deb" | head -1 || true)
if [ -n "$DEB" ]; then if [ -n "$DEB" ]; then
NEW_DEB="CC-Switch-${VERSION}-Linux.deb" cp "$DEB" "release-assets/CC-Switch-${VERSION}-Linux-${ARCH}.deb"
cp "$DEB" "release-assets/$NEW_DEB" echo "Deb package copied: CC-Switch-${VERSION}-Linux-${ARCH}.deb"
echo "Deb package copied: $NEW_DEB"
else else
echo "No .deb found (optional)" echo "No .deb found (optional)"
fi fi
# 额外上传 .rpm(用于 Fedora/RHEL/openSUSE 等,不参与 Updater # 额外上传 .rpm(用于 Fedora/RHEL/openSUSE 等,不参与 Updater
RPM=$(find src-tauri/target/release/bundle -name "*.rpm" | head -1 || true) RPM=$(find src-tauri/target/release/bundle -name "*.rpm" | head -1 || true)
if [ -n "$RPM" ]; then if [ -n "$RPM" ]; then
NEW_RPM="CC-Switch-${VERSION}-Linux.rpm" cp "$RPM" "release-assets/CC-Switch-${VERSION}-Linux-${ARCH}.rpm"
cp "$RPM" "release-assets/$NEW_RPM" echo "RPM package copied: CC-Switch-${VERSION}-Linux-${ARCH}.rpm"
echo "RPM package copied: $NEW_RPM"
else else
echo "No .rpm found (optional)" echo "No .rpm found (optional)"
fi fi
@@ -312,7 +313,8 @@ jobs:
- **macOS**: `CC-Switch-${{ github.ref_name }}-macOS.zip`(解压即用)或 `CC-Switch-${{ github.ref_name }}-macOS.tar.gz`Homebrew - **macOS**: `CC-Switch-${{ github.ref_name }}-macOS.zip`(解压即用)或 `CC-Switch-${{ github.ref_name }}-macOS.tar.gz`Homebrew
- **Windows**: `CC-Switch-${{ github.ref_name }}-Windows.msi`(安装版)或 `CC-Switch-${{ github.ref_name }}-Windows-Portable.zip`(绿色版) - **Windows**: `CC-Switch-${{ github.ref_name }}-Windows.msi`(安装版)或 `CC-Switch-${{ github.ref_name }}-Windows-Portable.zip`(绿色版)
- **Linux**: `CC-Switch-${{ github.ref_name }}-Linux.AppImage`AppImage)或 `CC-Switch-${{ github.ref_name }}-Linux.deb`Debian/Ubuntu)或 `CC-Switch-${{ github.ref_name }}-Linux.rpm`Fedora/RHEL/openSUSE - **Linux (x86_64)**: `CC-Switch-${{ github.ref_name }}-Linux-x86_64.AppImage` / `.deb` / `.rpm`
- **Linux (ARM64)**: `CC-Switch-${{ github.ref_name }}-Linux-arm64.AppImage` / `.deb` / `.rpm`
--- ---
提示:macOS 如遇"已损坏"提示,可在终端执行:`xattr -cr "/Applications/CC Switch.app"` 提示:macOS 如遇"已损坏"提示,可在终端执行:`xattr -cr "/Applications/CC Switch.app"`
@@ -360,7 +362,8 @@ jobs:
# 初始化空平台映射 # 初始化空平台映射
mac_url=""; mac_sig="" mac_url=""; mac_sig=""
win_url=""; win_sig="" win_url=""; win_sig=""
linux_url=""; linux_sig="" linux_x64_url=""; linux_x64_sig=""
linux_arm64_url=""; linux_arm64_sig=""
shopt -s nullglob shopt -s nullglob
for sig in dl/*.sig; do for sig in dl/*.sig; do
base=${sig%.sig} base=${sig%.sig}
@@ -371,8 +374,10 @@ jobs:
*.tar.gz) *.tar.gz)
# 视为 macOS updater artifact # 视为 macOS updater artifact
mac_url="$url"; mac_sig="$sig_content";; mac_url="$url"; mac_sig="$sig_content";;
*.AppImage|*.appimage) *-Linux-arm64.AppImage|*-Linux-arm64.appimage)
linux_url="$url"; linux_sig="$sig_content";; linux_arm64_url="$url"; linux_arm64_sig="$sig_content";;
*-Linux-x86_64.AppImage|*-Linux-x86_64.appimage)
linux_x64_url="$url"; linux_x64_sig="$sig_content";;
*.msi|*.exe) *.msi|*.exe)
win_url="$url"; win_sig="$sig_content";; win_url="$url"; win_sig="$sig_content";;
esac esac
@@ -399,9 +404,14 @@ jobs:
echo " \"windows-x86_64\": {\"signature\": \"$win_sig\", \"url\": \"$win_url\"}" echo " \"windows-x86_64\": {\"signature\": \"$win_sig\", \"url\": \"$win_url\"}"
first=0 first=0
fi fi
if [ -n "$linux_url" ] && [ -n "$linux_sig" ]; then if [ -n "$linux_x64_url" ] && [ -n "$linux_x64_sig" ]; then
[ $first -eq 0 ] && echo ',' [ $first -eq 0 ] && echo ','
echo " \"linux-x86_64\": {\"signature\": \"$linux_sig\", \"url\": \"$linux_url\"}" echo " \"linux-x86_64\": {\"signature\": \"$linux_x64_sig\", \"url\": \"$linux_x64_url\"}"
first=0
fi
if [ -n "$linux_arm64_url" ] && [ -n "$linux_arm64_sig" ]; then
[ $first -eq 0 ] && echo ','
echo " \"linux-aarch64\": {\"signature\": \"$linux_arm64_sig\", \"url\": \"$linux_arm64_url\"}"
first=0 first=0
fi fi
echo ' }' echo ' }'
+49
View File
@@ -9,6 +9,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
--- ---
## [3.10.3] - 2026-01-30
### Feature Release
This release introduces a generic API format selector, pricing configuration enhancements, and multiple UX improvements.
### Added
- **API Key Link for OpenCode**: API key link support for OpenCode provider form, enabling quick access to provider key management pages
- **AICodeMirror Partner Preset**: Added AICodeMirror partner preset for all apps (Claude, Codex, Gemini, OpenCode)
- **API Format Selector**: Generic API format chooser for Claude providers, replacing the OpenRouter-specific toggle. Supports Anthropic Messages (native) and OpenAI Chat Completions format
- **API Format Presets**: Allow preset providers to specify API format (anthropic or openai_chat) for third-party proxy services
- **Proxy Hint**: Display info toast when switching to OpenAI Chat format provider, reminding users to enable proxy
- **Pricing Config Enhancement**: Per-provider cost multiplier, pricing model source (request/response), request model logging, and enriched usage UI (#781)
- **Skills ZIP Install**: Install skills directly from local ZIP files with recursive scanning support
- **Preferred Terminal**: Choose preferred terminal app per platform (macOS: Terminal.app/iTerm2/Alacritty/Kitty/Ghostty; Windows: cmd/PowerShell/Windows Terminal; Linux: GNOME Terminal/Konsole/Xfce4/Alacritty/Kitty/Ghostty)
- **Silent Startup**: Option to prevent window popup on launch (#713)
- **OpenCode Environment Check**: Version detection with Go path scanning and one-click install from GitHub Releases
- **OpenCode Directory Sync**: Auto-sync all providers to live config on directory change with additive mode support
- **NVIDIA NIM Preset**: New provider preset for Claude and OpenCode with nvidia.svg icon
- **n1n.ai Preset**: New provider preset (#667)
- **Update Badge Icon**: Replace update badge dot with ArrowUpCircle icon
- **Linux ARM64**: CI build support for Linux ARM64 architecture
### Changed
- **API Format Migration**: Migrate api_format from settings_config to ProviderMeta to prevent polluting ~/.claude/settings.json
- **DeepSeek max_tokens**: Remove max_tokens clamp from proxy transform layer
- **Terminal Functions**: Consolidate redundant terminal launch functions
- **Home Dir Utility**: Consolidate get_home_dir into single public function
- **Kimi/Moonshot**: Upgrade provider presets to k2.5 model
### Fixed
- **Codex 404 & Timeout**: Fix 404 errors and connection timeout with custom base_url; improve /v1 prefix handling and system proxy detection (#760)
- **Proxy URL Building**: Fix duplicate /v1/v1 in URL; extend ?beta=true to /v1/chat/completions endpoint
- **OpenRouter Compat Mode**: Improve backward compatibility supporting number and string types
- **Gemini Visibility**: Correct Gemini default visibility to true (#818)
- **Footer Layout**: Correct footer layout in advanced settings tab
- **Claude Code Detection**: Prioritize native install path for detection
- **Tray Menu**: Simplify title labels and optimize menu separators (#796)
- **Duplicate Skills**: Prevent duplicate skill installation from different repos (#778)
- **Windows Tests**: Stabilize test environment (#644)
- **i18n**: Update apiFormatOpenAIChat label to mention proxy requirement
- **Error Display**: Use extractErrorMessage for complete error display in mutations
- **Sponsors**: Add AICodeMirror and reorder sponsor list
---
## [3.10.2] - 2026-01-24 ## [3.10.2] - 2026-01-24
### Patch Release ### Patch Release
@@ -0,0 +1,162 @@
/**
* 统一供应商(Universal Provider)预设配置
*
* 统一供应商是跨应用共享的配置,修改后会自动同步到 Claude、Codex、Gemini 三个应用。
* 适用于 NewAPI 等支持多种协议的 API 网关。
*/
import type {
UniversalProvider,
UniversalProviderApps,
UniversalProviderModels,
} from "@/types";
/**
* 统一供应商预设接口
*/
export interface UniversalProviderPreset {
/** 预设名称 */
name: string;
/** 供应商类型标识 */
providerType: string;
/** 默认启用的应用 */
defaultApps: UniversalProviderApps;
/** 默认模型配置 */
defaultModels: UniversalProviderModels;
/** 网站链接 */
websiteUrl?: string;
/** 图标名称 */
icon?: string;
/** 图标颜色 */
iconColor?: string;
/** 描述 */
description?: string;
/** 是否为自定义模板(允许用户完全自定义) */
isCustomTemplate?: boolean;
}
/**
* NewAPI 默认模型配置
*/
const NEWAPI_DEFAULT_MODELS: UniversalProviderModels = {
claude: {
model: "claude-sonnet-4-20250514",
haikuModel: "claude-haiku-4-20250514",
sonnetModel: "claude-sonnet-4-20250514",
opusModel: "claude-sonnet-4-20250514",
},
codex: {
model: "gpt-4o",
reasoningEffort: "high",
},
gemini: {
model: "gemini-2.5-pro",
},
};
const N1N_DEFAULT_MODELS: UniversalProviderModels = {
claude: {
model: "claude-3-5-sonnet-20240620",
haikuModel: "claude-3-haiku-20240307",
sonnetModel: "claude-3-5-sonnet-20240620",
opusModel: "claude-3-opus-20240229",
},
codex: {
model: "gpt-4o",
reasoningEffort: "high",
},
gemini: {
model: "gemini-1.5-pro-latest",
},
};
/**
* 统一供应商预设列表
*/
export const universalProviderPresets: UniversalProviderPreset[] = [
{
name: "n1n.ai",
providerType: "n1n",
defaultApps: {
claude: true,
codex: true,
gemini: true,
},
defaultModels: N1N_DEFAULT_MODELS,
websiteUrl: "https://n1n.ai",
icon: "openai",
iconColor: "#000000",
description:
"n1n.ai - 聚合 OpenAI, Anthropic, Google 等主流大模型的一站式 AI 服务平台",
},
{
name: "NewAPI",
providerType: "newapi",
defaultApps: {
claude: true,
codex: true,
gemini: true,
},
defaultModels: NEWAPI_DEFAULT_MODELS,
websiteUrl: "https://www.newapi.pro",
icon: "newapi",
iconColor: "#00A67E",
description:
"NewAPI 是一个可自部署的 API 网关,支持 Anthropic、OpenAI、Gemini 等多种协议",
},
{
name: "自定义网关",
providerType: "custom_gateway",
defaultApps: {
claude: true,
codex: true,
gemini: true,
},
defaultModels: NEWAPI_DEFAULT_MODELS,
icon: "openai",
iconColor: "#6366F1",
description: "自定义配置的 API 网关",
isCustomTemplate: true,
},
];
/**
* 根据预设创建统一供应商
*/
export function createUniversalProviderFromPreset(
preset: UniversalProviderPreset,
id: string,
baseUrl: string,
apiKey: string,
customName?: string,
): UniversalProvider {
return {
id,
name: customName || preset.name,
providerType: preset.providerType,
apps: { ...preset.defaultApps },
baseUrl,
apiKey,
models: JSON.parse(JSON.stringify(preset.defaultModels)), // Deep copy
websiteUrl: preset.websiteUrl,
icon: preset.icon,
iconColor: preset.iconColor,
createdAt: Date.now(),
};
}
/**
* 获取预设的显示名称(用于 UI)
*/
export function getPresetDisplayName(preset: UniversalProviderPreset): string {
return preset.name;
}
/**
* 根据类型查找预设
*/
export function findPresetByType(
providerType: string,
): UniversalProviderPreset | undefined {
return universalProviderPresets.find((p) => p.providerType === providerType);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cc-switch", "name": "cc-switch",
"version": "3.10.2", "version": "3.10.3",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI", "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -701,7 +701,7 @@ dependencies = [
[[package]] [[package]]
name = "cc-switch" name = "cc-switch"
version = "3.10.2" version = "3.10.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "cc-switch" name = "cc-switch"
version = "3.10.2" version = "3.10.3"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI" description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"] authors = ["Jason Young"]
license = "MIT" license = "MIT"
+35 -2
View File
@@ -89,7 +89,7 @@ pub struct ToolVersion {
#[tauri::command] #[tauri::command]
pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> { pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
let tools = vec!["claude", "codex", "gemini"]; let tools = vec!["claude", "codex", "gemini", "opencode"];
let mut results = Vec::new(); let mut results = Vec::new();
// 使用全局 HTTP 客户端(已包含代理配置) // 使用全局 HTTP 客户端(已包含代理配置)
@@ -116,6 +116,7 @@ pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
"claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await, "claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await,
"codex" => fetch_npm_latest_version(&client, "@openai/codex").await, "codex" => fetch_npm_latest_version(&client, "@openai/codex").await,
"gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await, "gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await,
"opencode" => fetch_github_latest_version(&client, "anomalyco/opencode").await,
_ => None, _ => None,
}; };
@@ -148,6 +149,29 @@ async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Op
} }
} }
/// Helper function to fetch latest version from GitHub releases
async fn fetch_github_latest_version(client: &reqwest::Client, repo: &str) -> Option<String> {
let url = format!("https://api.github.com/repos/{repo}/releases/latest");
match client
.get(&url)
.header("User-Agent", "cc-switch")
.header("Accept", "application/vnd.github+json")
.send()
.await
{
Ok(resp) => {
if let Ok(json) = resp.json::<serde_json::Value>().await {
json.get("tag_name")
.and_then(|v| v.as_str())
.map(|s| s.strip_prefix('v').unwrap_or(s).to_string())
} else {
None
}
}
Err(_) => None,
}
}
/// 预编译的版本号正则表达式 /// 预编译的版本号正则表达式
static VERSION_RE: Lazy<Regex> = static VERSION_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").expect("Invalid version regex")); Lazy::new(|| Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").expect("Invalid version regex"));
@@ -224,7 +248,7 @@ fn try_get_version_wsl(tool: &str, distro: &str) -> (Option<String>, Option<Stri
// 防御性断言:tool 只能是预定义的值 // 防御性断言:tool 只能是预定义的值
debug_assert!( debug_assert!(
["claude", "codex", "gemini"].contains(&tool), ["claude", "codex", "gemini", "opencode"].contains(&tool),
"unexpected tool name: {tool}" "unexpected tool name: {tool}"
); );
@@ -348,6 +372,14 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
} }
} }
// 添加 Go 路径支持 (opencode 使用 go install 安装)
if tool == "opencode" {
search_paths.push(home.join("go/bin")); // go install 默认路径
if let Ok(gopath) = std::env::var("GOPATH") {
search_paths.push(std::path::PathBuf::from(gopath).join("bin"));
}
}
// 在每个路径中查找工具 // 在每个路径中查找工具
for path in &search_paths { for path in &search_paths {
let tool_path = if cfg!(target_os = "windows") { let tool_path = if cfg!(target_os = "windows") {
@@ -405,6 +437,7 @@ fn wsl_distro_for_tool(tool: &str) -> Option<String> {
"claude" => crate::settings::get_claude_override_dir(), "claude" => crate::settings::get_claude_override_dir(),
"codex" => crate::settings::get_codex_override_dir(), "codex" => crate::settings::get_codex_override_dir(),
"gemini" => crate::settings::get_gemini_override_dir(), "gemini" => crate::settings::get_gemini_override_dir(),
"opencode" => crate::settings::get_opencode_override_dir(),
_ => None, _ => None,
}?; }?;
+48 -48
View File
@@ -253,6 +253,11 @@ pub struct ProviderMeta {
skip_serializing_if = "Option::is_none" skip_serializing_if = "Option::is_none"
)] )]
pub common_config_enabled_by_app: Option<CommonConfigEnabledByApp>, pub common_config_enabled_by_app: Option<CommonConfigEnabledByApp>,
/// Claude API 格式(仅 Claude 供应商使用)
/// - "anthropic": 原生 Anthropic Messages API,直接透传
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
pub api_format: Option<String>,
} }
impl ProviderManager { impl ProviderManager {
@@ -551,54 +556,6 @@ requires_openai_auth = true"#
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn universal_codex_provider_origin_base_url_adds_v1() {
let mut p = UniversalProvider::new(
"id".to_string(),
"Test".to_string(),
"custom".to_string(),
"https://api.openai.com".to_string(),
"sk-test".to_string(),
);
p.apps.codex = true;
let provider = p.to_codex_provider().expect("should build codex provider");
let toml = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
.expect("config should be a toml string");
assert!(toml.contains("base_url = \"https://api.openai.com/v1\""));
}
#[test]
fn universal_codex_provider_custom_prefix_does_not_force_v1() {
let mut p = UniversalProvider::new(
"id".to_string(),
"Test".to_string(),
"custom".to_string(),
"https://example.com/openai".to_string(),
"sk-test".to_string(),
);
p.apps.codex = true;
let provider = p.to_codex_provider().expect("should build codex provider");
let toml = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
.expect("config should be a toml string");
assert!(toml.contains("base_url = \"https://example.com/openai\""));
assert!(!toml.contains("https://example.com/openai/v1"));
}
}
// ============================================================================ // ============================================================================
// OpenCode 供应商配置结构 // OpenCode 供应商配置结构
// ============================================================================ // ============================================================================
@@ -958,4 +915,47 @@ mod provider_tests {
assert!(config.options.headers.is_none()); assert!(config.options.headers.is_none());
assert!(config.options.extra.is_empty()); assert!(config.options.extra.is_empty());
} }
#[test]
fn universal_codex_provider_origin_base_url_adds_v1() {
let mut p = UniversalProvider::new(
"id".to_string(),
"Test".to_string(),
"custom".to_string(),
"https://api.openai.com".to_string(),
"sk-test".to_string(),
);
p.apps.codex = true;
let provider = p.to_codex_provider().expect("should build codex provider");
let toml = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
.expect("config should be a toml string");
assert!(toml.contains("base_url = \"https://api.openai.com/v1\""));
}
#[test]
fn universal_codex_provider_custom_prefix_does_not_force_v1() {
let mut p = UniversalProvider::new(
"id".to_string(),
"Test".to_string(),
"custom".to_string(),
"https://example.com/openai".to_string(),
"sk-test".to_string(),
);
p.apps.codex = true;
let provider = p.to_codex_provider().expect("should build codex provider");
let toml = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
.expect("config should be a toml string");
assert!(toml.contains("base_url = \"https://example.com/openai\""));
assert!(!toml.contains("https://example.com/openai/v1"));
}
} }
+5 -1
View File
@@ -666,7 +666,11 @@ impl RequestForwarder {
// 输出请求信息日志 // 输出请求信息日志
let tag = adapter.name(); let tag = adapter.name();
log::info!("[{tag}] >>> 请求 URL: {url}"); let request_model = filtered_body
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("<none>");
log::info!("[{tag}] >>> 请求 URL: {url} (model={request_model})");
if let Ok(body_str) = serde_json::to_string(&filtered_body) { if let Ok(body_str) = serde_json::to_string(&filtered_body) {
log::debug!( log::debug!(
"[{tag}] >>> 请求体内容 ({}字节): {}", "[{tag}] >>> 请求体内容 ({}字节): {}",
+162 -36
View File
@@ -1,11 +1,15 @@
//! Claude (Anthropic) Provider Adapter //! Claude (Anthropic) Provider Adapter
//! //!
//! 支持透传模式和 OpenRouter 兼容模式 //! 支持透传模式和 OpenAI Chat Completions 格式转换模式
//!
//! ## API 格式
//! - **anthropic** (默认): Anthropic Messages API 格式,直接透传
//! - **openai_chat**: OpenAI Chat Completions 格式,需要 Anthropic ↔ OpenAI 转换
//! //!
//! ## 认证模式 //! ## 认证模式
//! - **Claude**: Anthropic 官方 API (x-api-key + anthropic-version) //! - **Claude**: Anthropic 官方 API (x-api-key + anthropic-version)
//! - **ClaudeAuth**: 中转服务 (仅 Bearer 认证,无 x-api-key) //! - **ClaudeAuth**: 中转服务 (仅 Bearer 认证,无 x-api-key)
//! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传(保留旧转换逻辑备用) //! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传
use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType}; use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType};
use crate::provider::Provider; use crate::provider::Provider;
@@ -48,22 +52,52 @@ impl ClaudeAdapter {
false false
} }
/// 检测 OpenRouter 是否启用兼容模 /// 获取 API 格
fn is_openrouter_compat_enabled(&self, provider: &Provider) -> bool { ///
if !self.is_openrouter(provider) { /// 从 provider.meta.api_format 读取格式设置:
return false; /// - "anthropic" (默认): Anthropic Messages API 格式,直接透传
/// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
fn get_api_format(&self, provider: &Provider) -> &'static str {
// 1) Preferred: meta.apiFormat (SSOT, never written to Claude Code config)
if let Some(meta) = provider.meta.as_ref() {
if let Some(api_format) = meta.api_format.as_deref() {
return if api_format == "openai_chat" {
"openai_chat"
} else {
"anthropic"
};
}
} }
// 2) Backward compatibility: legacy settings_config.api_format
if let Some(api_format) = provider
.settings_config
.get("api_format")
.and_then(|v| v.as_str())
{
return if api_format == "openai_chat" {
"openai_chat"
} else {
"anthropic"
};
}
// 3) Backward compatibility: legacy openrouter_compat_mode (bool/number/string)
let raw = provider.settings_config.get("openrouter_compat_mode"); let raw = provider.settings_config.get("openrouter_compat_mode");
match raw { let enabled = match raw {
Some(serde_json::Value::Bool(enabled)) => *enabled, Some(serde_json::Value::Bool(v)) => *v,
Some(serde_json::Value::Number(num)) => num.as_i64().unwrap_or(0) != 0, Some(serde_json::Value::Number(num)) => num.as_i64().unwrap_or(0) != 0,
Some(serde_json::Value::String(value)) => { Some(serde_json::Value::String(value)) => {
let normalized = value.trim().to_lowercase(); let normalized = value.trim().to_lowercase();
normalized == "true" || normalized == "1" normalized == "true" || normalized == "1"
} }
// OpenRouter now supports Claude Code compatible API, default to passthrough
_ => false, _ => false,
};
if enabled {
"openai_chat"
} else {
"anthropic"
} }
} }
@@ -218,15 +252,23 @@ impl ProviderAdapter for ClaudeAdapter {
// 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。 // 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。
// 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。 // 如需回退旧逻辑,可在 forwarder 中根据 needs_transform 改写 endpoint。
let base = format!( let mut base = format!(
"{}/{}", "{}/{}",
base_url.trim_end_matches('/'), base_url.trim_end_matches('/'),
endpoint.trim_start_matches('/') endpoint.trim_start_matches('/')
); );
// 为 /v1/messages 端点添加 ?beta=true 参数 // 去除重复的 /v1/v1(可能由 base_url 与 endpoint 都带版本导致)
while base.contains("/v1/v1") {
base = base.replace("/v1/v1", "/v1");
}
// 为 Claude 相关端点添加 ?beta=true 参数
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数 // 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
if endpoint.contains("/v1/messages") && !endpoint.contains("?") { // 注:openai_chat 模式下会转发到 /v1/chat/completions,此处也需要保持一致
if (endpoint.contains("/v1/messages") || endpoint.contains("/v1/chat/completions"))
&& !endpoint.contains('?')
{
format!("{base}?beta=true") format!("{base}?beta=true")
} else { } else {
base base
@@ -253,21 +295,19 @@ impl ProviderAdapter for ClaudeAdapter {
} }
} }
fn needs_transform(&self, _provider: &Provider) -> bool { fn needs_transform(&self, provider: &Provider) -> bool {
// NOTE: // 根据 api_format 配置决定是否需要格式转换
// OpenRouter 已推出 Claude Code 兼容接口(可直接处理 `/v1/messages`),默认不再启用 // - "anthropic" (默认): 直接透传,无需转换
// Anthropic ↔ OpenAI 格式转换 // - "openai_chat": 需要 Anthropic ↔ OpenAI 格式转换
// self.get_api_format(provider) == "openai_chat"
// 如果未来需要回退到旧的 OpenAI Chat Completions 方案,可恢复下面这行:
self.is_openrouter_compat_enabled(_provider)
} }
fn transform_request( fn transform_request(
&self, &self,
body: serde_json::Value, body: serde_json::Value,
provider: &Provider, _provider: &Provider,
) -> Result<serde_json::Value, ProxyError> { ) -> Result<serde_json::Value, ProxyError> {
super::transform::anthropic_to_openai(body, provider) super::transform::anthropic_to_openai(body)
} }
fn transform_response(&self, body: serde_json::Value) -> Result<serde_json::Value, ProxyError> { fn transform_response(&self, body: serde_json::Value) -> Result<serde_json::Value, ProxyError> {
@@ -278,6 +318,7 @@ impl ProviderAdapter for ClaudeAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::provider::ProviderMeta;
use serde_json::json; use serde_json::json;
fn create_provider(config: serde_json::Value) -> Provider { fn create_provider(config: serde_json::Value) -> Provider {
@@ -297,6 +338,23 @@ mod tests {
} }
} }
fn create_provider_with_meta(config: serde_json::Value, meta: ProviderMeta) -> Provider {
Provider {
id: "test".to_string(),
name: "Test Claude".to_string(),
settings_config: config,
website_url: None,
category: Some("claude".to_string()),
created_at: None,
sort_index: None,
notes: None,
meta: Some(meta),
icon: None,
icon_color: None,
in_failover_queue: false,
}
}
#[test] #[test]
fn test_extract_base_url_from_env() { fn test_extract_base_url_from_env() {
let adapter = ClaudeAdapter::new(); let adapter = ClaudeAdapter::new();
@@ -459,6 +517,7 @@ mod tests {
fn test_needs_transform() { fn test_needs_transform() {
let adapter = ClaudeAdapter::new(); let adapter = ClaudeAdapter::new();
// Default: no transform (anthropic format) - no meta
let anthropic_provider = create_provider(json!({ let anthropic_provider = create_provider(json!({
"env": { "env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com" "ANTHROPIC_BASE_URL": "https://api.anthropic.com"
@@ -466,29 +525,96 @@ mod tests {
})); }));
assert!(!adapter.needs_transform(&anthropic_provider)); assert!(!adapter.needs_transform(&anthropic_provider));
// OpenRouter provider without explicit setting now defaults to passthrough (no transform) // Explicit anthropic format in meta: no transform
let openrouter_provider = create_provider(json!({ let explicit_anthropic = create_provider_with_meta(
"env": { json!({
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api" "env": {
} "ANTHROPIC_BASE_URL": "https://api.example.com"
})); }
assert!(!adapter.needs_transform(&openrouter_provider)); }),
ProviderMeta {
api_format: Some("anthropic".to_string()),
..Default::default()
},
);
assert!(!adapter.needs_transform(&explicit_anthropic));
// OpenRouter provider with explicit compat mode enabled should transform // Legacy settings_config.api_format: openai_chat should enable transform
let openrouter_enabled = create_provider(json!({ let legacy_settings_api_format = create_provider(json!({
"env": { "env": {
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api" "ANTHROPIC_BASE_URL": "https://api.example.com"
},
"api_format": "openai_chat"
}));
assert!(adapter.needs_transform(&legacy_settings_api_format));
// Legacy openrouter_compat_mode: bool/number/string should enable transform
let legacy_openrouter_bool = create_provider(json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com"
}, },
"openrouter_compat_mode": true "openrouter_compat_mode": true
})); }));
assert!(adapter.needs_transform(&openrouter_enabled)); assert!(adapter.needs_transform(&legacy_openrouter_bool));
let openrouter_disabled = create_provider(json!({ let legacy_openrouter_num = create_provider(json!({
"env": { "env": {
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api" "ANTHROPIC_BASE_URL": "https://api.example.com"
}, },
"openrouter_compat_mode": false "openrouter_compat_mode": 1
})); }));
assert!(!adapter.needs_transform(&openrouter_disabled)); assert!(adapter.needs_transform(&legacy_openrouter_num));
let legacy_openrouter_str = create_provider(json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com"
},
"openrouter_compat_mode": "true"
}));
assert!(adapter.needs_transform(&legacy_openrouter_str));
// OpenAI Chat format in meta: needs transform
let openai_chat_provider = create_provider_with_meta(
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com"
}
}),
ProviderMeta {
api_format: Some("openai_chat".to_string()),
..Default::default()
},
);
assert!(adapter.needs_transform(&openai_chat_provider));
// meta takes precedence over legacy settings_config fields
let meta_precedence_over_settings = create_provider_with_meta(
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com"
},
"api_format": "openai_chat",
"openrouter_compat_mode": true
}),
ProviderMeta {
api_format: Some("anthropic".to_string()),
..Default::default()
},
);
assert!(!adapter.needs_transform(&meta_precedence_over_settings));
// Unknown format in meta: default to anthropic (no transform)
let unknown_format = create_provider_with_meta(
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com"
}
}),
ProviderMeta {
api_format: Some("unknown".to_string()),
..Default::default()
},
);
assert!(!adapter.needs_transform(&unknown_format));
} }
} }
+14 -171
View File
@@ -3,77 +3,16 @@
//! 实现 Anthropic ↔ OpenAI 格式转换,用于 OpenRouter 支持 //! 实现 Anthropic ↔ OpenAI 格式转换,用于 OpenRouter 支持
//! 参考: anthropic-proxy-rs //! 参考: anthropic-proxy-rs
use crate::provider::Provider;
use crate::proxy::error::ProxyError; use crate::proxy::error::ProxyError;
use serde_json::{json, Value}; use serde_json::{json, Value};
/// 从 Provider 配置中获取模型映射
fn get_model_from_provider(model: &str, provider: &Provider, body: &Value) -> String {
let env = provider.settings_config.get("env");
let model_lower = model.to_lowercase();
// 检测 thinking 参数
let has_thinking = body
.get("thinking")
.and_then(|v| v.as_object())
.and_then(|o| o.get("type"))
.and_then(|t| t.as_str())
== Some("enabled");
if let Some(env) = env {
// 如果启用 thinking,优先使用推理模型
if has_thinking {
if let Some(m) = env
.get("ANTHROPIC_REASONING_MODEL")
.and_then(|v| v.as_str())
{
log::debug!("[Transform] 使用推理模型: {m}");
return m.to_string();
}
}
// 根据模型类型选择配置模型
if model_lower.contains("haiku") {
if let Some(m) = env
.get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
.and_then(|v| v.as_str())
{
return m.to_string();
}
}
if model_lower.contains("opus") {
if let Some(m) = env
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
.and_then(|v| v.as_str())
{
return m.to_string();
}
}
if model_lower.contains("sonnet") {
if let Some(m) = env
.get("ANTHROPIC_DEFAULT_SONNET_MODEL")
.and_then(|v| v.as_str())
{
return m.to_string();
}
}
// 默认使用 ANTHROPIC_MODEL
if let Some(m) = env.get("ANTHROPIC_MODEL").and_then(|v| v.as_str()) {
return m.to_string();
}
}
model.to_string()
}
/// Anthropic 请求 → OpenAI 请求 /// Anthropic 请求 → OpenAI 请求
pub fn anthropic_to_openai(body: Value, provider: &Provider) -> Result<Value, ProxyError> { pub fn anthropic_to_openai(body: Value) -> Result<Value, ProxyError> {
let mut result = json!({}); let mut result = json!({});
// 模型映射:使用 Provider 配置中的模型(支持 thinking 参数) // NOTE: 模型映射由上游统一处理(proxy::model_mapper),格式转换层只做结构转换。
if let Some(model) = body.get("model").and_then(|m| m.as_str()) { if let Some(model) = body.get("model").and_then(|m| m.as_str()) {
let mapped_model = get_model_from_provider(model, provider, &body); result["model"] = json!(model);
result["model"] = json!(mapped_model);
} }
let mut messages = Vec::new(); let mut messages = Vec::new();
@@ -381,45 +320,16 @@ pub fn openai_to_anthropic(body: Value) -> Result<Value, ProxyError> {
mod tests { mod tests {
use super::*; use super::*;
fn create_provider(env_config: Value) -> Provider {
Provider {
id: "test".to_string(),
name: "Test Provider".to_string(),
settings_config: json!({"env": env_config}),
website_url: None,
category: None,
created_at: None,
sort_index: None,
notes: None,
meta: None,
icon: None,
icon_color: None,
in_failover_queue: false,
}
}
fn create_openrouter_provider() -> Provider {
create_provider(json!({
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
"ANTHROPIC_MODEL": "anthropic/claude-sonnet-4.5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "anthropic/claude-haiku-4.5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "anthropic/claude-sonnet-4.5",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "anthropic/claude-opus-4.5"
}))
}
#[test] #[test]
fn test_anthropic_to_openai_simple() { fn test_anthropic_to_openai_simple() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-3-opus", "model": "claude-3-opus",
"max_tokens": 1024, "max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}] "messages": [{"role": "user", "content": "Hello"}]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
// opus 模型映射到配置的 ANTHROPIC_DEFAULT_OPUS_MODEL assert_eq!(result["model"], "claude-3-opus");
assert_eq!(result["model"], "anthropic/claude-opus-4.5");
assert_eq!(result["max_tokens"], 1024); assert_eq!(result["max_tokens"], 1024);
assert_eq!(result["messages"][0]["role"], "user"); assert_eq!(result["messages"][0]["role"], "user");
assert_eq!(result["messages"][0]["content"], "Hello"); assert_eq!(result["messages"][0]["content"], "Hello");
@@ -427,7 +337,6 @@ mod tests {
#[test] #[test]
fn test_anthropic_to_openai_with_system() { fn test_anthropic_to_openai_with_system() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-3-sonnet", "model": "claude-3-sonnet",
"max_tokens": 1024, "max_tokens": 1024,
@@ -435,7 +344,7 @@ mod tests {
"messages": [{"role": "user", "content": "Hello"}] "messages": [{"role": "user", "content": "Hello"}]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["messages"][0]["role"], "system"); assert_eq!(result["messages"][0]["role"], "system");
assert_eq!( assert_eq!(
result["messages"][0]["content"], result["messages"][0]["content"],
@@ -446,7 +355,6 @@ mod tests {
#[test] #[test]
fn test_anthropic_to_openai_with_tools() { fn test_anthropic_to_openai_with_tools() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-3-opus", "model": "claude-3-opus",
"max_tokens": 1024, "max_tokens": 1024,
@@ -458,14 +366,13 @@ mod tests {
}] }]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["tools"][0]["type"], "function"); assert_eq!(result["tools"][0]["type"], "function");
assert_eq!(result["tools"][0]["function"]["name"], "get_weather"); assert_eq!(result["tools"][0]["function"]["name"], "get_weather");
} }
#[test] #[test]
fn test_anthropic_to_openai_tool_use() { fn test_anthropic_to_openai_tool_use() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-3-opus", "model": "claude-3-opus",
"max_tokens": 1024, "max_tokens": 1024,
@@ -478,7 +385,7 @@ mod tests {
}] }]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
let msg = &result["messages"][0]; let msg = &result["messages"][0];
assert_eq!(msg["role"], "assistant"); assert_eq!(msg["role"], "assistant");
assert!(msg.get("tool_calls").is_some()); assert!(msg.get("tool_calls").is_some());
@@ -487,7 +394,6 @@ mod tests {
#[test] #[test]
fn test_anthropic_to_openai_tool_result() { fn test_anthropic_to_openai_tool_result() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-3-opus", "model": "claude-3-opus",
"max_tokens": 1024, "max_tokens": 1024,
@@ -499,7 +405,7 @@ mod tests {
}] }]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
let msg = &result["messages"][0]; let msg = &result["messages"][0];
assert_eq!(msg["role"], "tool"); assert_eq!(msg["role"], "tool");
assert_eq!(msg["tool_call_id"], "call_123"); assert_eq!(msg["tool_call_id"], "call_123");
@@ -563,78 +469,15 @@ mod tests {
} }
#[test] #[test]
fn test_model_mapping_from_provider() { fn test_model_passthrough() {
let provider = create_openrouter_provider(); // 格式转换层只做结构转换,模型映射由上游 proxy::model_mapper 处理
let body = json!({"model": "test"});
// sonnet 模型
assert_eq!(
get_model_from_provider("claude-sonnet-4-5-20250929", &provider, &body),
"anthropic/claude-sonnet-4.5"
);
// haiku 模型
assert_eq!(
get_model_from_provider("claude-haiku-4-5-20250929", &provider, &body),
"anthropic/claude-haiku-4.5"
);
// opus 模型
assert_eq!(
get_model_from_provider("claude-opus-4-5", &provider, &body),
"anthropic/claude-opus-4.5"
);
}
#[test]
fn test_anthropic_to_openai_model_mapping() {
let provider = create_openrouter_provider();
let input = json!({ let input = json!({
"model": "claude-sonnet-4-5-20250929", "model": "gpt-4o",
"max_tokens": 1024, "max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}] "messages": [{"role": "user", "content": "Hello"}]
}); });
let result = anthropic_to_openai(input, &provider).unwrap(); let result = anthropic_to_openai(input).unwrap();
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5"); assert_eq!(result["model"], "gpt-4o");
}
#[test]
fn test_thinking_parameter_detection() {
let mut provider = create_openrouter_provider();
// 添加推理模型配置
if let Some(env) = provider.settings_config.get_mut("env") {
env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended");
}
let input = json!({
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"thinking": {"type": "enabled"},
"messages": [{"role": "user", "content": "Solve this problem"}]
});
let result = anthropic_to_openai(input, &provider).unwrap();
// 应该使用推理模型
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5:extended");
}
#[test]
fn test_thinking_parameter_disabled() {
let mut provider = create_openrouter_provider();
if let Some(env) = provider.settings_config.get_mut("env") {
env["ANTHROPIC_REASONING_MODEL"] = json!("anthropic/claude-sonnet-4.5:extended");
}
let input = json!({
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"thinking": {"type": "disabled"},
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_openai(input, &provider).unwrap();
// 应该使用普通模型
assert_eq!(result["model"], "anthropic/claude-sonnet-4.5");
} }
} }
+3 -2
View File
@@ -1,4 +1,4 @@
use super::provider::ProviderService; use super::provider::{sanitize_claude_settings_for_live, ProviderService};
use crate::app_config::{AppType, MultiAppConfig}; use crate::app_config::{AppType, MultiAppConfig};
use crate::error::AppError; use crate::error::AppError;
use crate::provider::Provider; use crate::provider::Provider;
@@ -181,7 +181,8 @@ impl ConfigService {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
} }
write_json_file(&settings_path, &provider.settings_config)?; let settings = sanitize_claude_settings_for_live(&provider.settings_config);
write_json_file(&settings_path, &settings)?;
let live_after = read_json_file::<serde_json::Value>(&settings_path)?; let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
if let Some(manager) = config.get_manager_mut(&AppType::Claude) { if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
+14 -1
View File
@@ -28,6 +28,18 @@ use super::gemini_auth::{
}; };
use super::normalize_claude_models_in_value; use super::normalize_claude_models_in_value;
pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value {
let mut v = settings.clone();
if let Some(obj) = v.as_object_mut() {
// Internal-only fields - never write to Claude Code settings.json
obj.remove("api_format");
obj.remove("apiFormat");
obj.remove("openrouter_compat_mode");
obj.remove("openrouterCompatMode");
}
v
}
/// Live configuration snapshot for backup/restore /// Live configuration snapshot for backup/restore
#[derive(Clone)] #[derive(Clone)]
#[allow(dead_code)] #[allow(dead_code)]
@@ -163,7 +175,8 @@ fn write_live_snapshot_internal(
match app_type { match app_type {
AppType::Claude => { AppType::Claude => {
let path = get_claude_settings_path(); let path = get_claude_settings_path();
write_json_file(&path, config_to_write)?; let settings = sanitize_claude_settings_for_live(config_to_write);
write_json_file(&path, &settings)?;
} }
AppType::Codex => { AppType::Codex => {
let obj = config_to_write.as_object().ok_or_else(|| { let obj = config_to_write.as_object().ok_or_else(|| {
+1
View File
@@ -29,6 +29,7 @@ pub use live::{
}; };
// Internal re-exports (pub(crate)) // Internal re-exports (pub(crate))
pub(crate) use live::sanitize_claude_settings_for_live;
pub(crate) use live::{write_live_snapshot, write_live_snapshot_with_merge}; pub(crate) use live::{write_live_snapshot, write_live_snapshot_with_merge};
// Internal re-exports // Internal re-exports
+2 -1
View File
@@ -1699,7 +1699,8 @@ impl ProxyService {
fn write_claude_live(&self, config: &Value) -> Result<(), String> { fn write_claude_live(&self, config: &Value) -> Result<(), String> {
let path = get_claude_settings_path(); let path = get_claude_settings_path();
write_json_file(&path, config).map_err(|e| format!("写入 Claude 配置失败: {e}")) let settings = crate::services::provider::sanitize_claude_settings_for_live(config);
write_json_file(&path, &settings).map_err(|e| format!("写入 Claude 配置失败: {e}"))
} }
fn read_codex_live(&self) -> Result<Value, String> { fn read_codex_live(&self) -> Result<Value, String> {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch", "productName": "CC Switch",
"version": "3.10.2", "version": "3.10.3",
"identifier": "com.ccswitch.desktop", "identifier": "com.ccswitch.desktop",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
@@ -1,10 +1,16 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form"; import { FormLabel } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import EndpointSpeedTest from "./EndpointSpeedTest"; import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField } from "./shared"; import { ApiKeySection, EndpointField } from "./shared";
import type { ProviderCategory } from "@/types"; import type { ProviderCategory, ClaudeApiFormat } from "@/types";
import type { TemplateValueConfig } from "@/config/claudeProviderPresets"; import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
interface EndpointCandidate { interface EndpointCandidate {
@@ -59,10 +65,9 @@ interface ClaudeFormFieldsProps {
// Speed Test Endpoints // Speed Test Endpoints
speedTestEndpoints: EndpointCandidate[]; speedTestEndpoints: EndpointCandidate[];
// OpenRouter Compat // API Format (for third-party providers that use OpenAI Chat Completions format)
showOpenRouterCompatToggle: boolean; apiFormat: ClaudeApiFormat;
openRouterCompatEnabled: boolean; onApiFormatChange: (format: ClaudeApiFormat) => void;
onOpenRouterCompatChange: (enabled: boolean) => void;
} }
export function ClaudeFormFields({ export function ClaudeFormFields({
@@ -95,9 +100,8 @@ export function ClaudeFormFields({
defaultOpusModel, defaultOpusModel,
onModelChange, onModelChange,
speedTestEndpoints, speedTestEndpoints,
showOpenRouterCompatToggle, apiFormat,
openRouterCompatEnabled, onApiFormatChange,
onOpenRouterCompatChange,
}: ClaudeFormFieldsProps) { }: ClaudeFormFieldsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -180,25 +184,34 @@ export function ClaudeFormFields({
/> />
)} )}
{showOpenRouterCompatToggle && ( {/* API 格式选择(仅非官方供应商显示) */}
<div className="flex items-center justify-between rounded-lg border border-white/10 bg-background/60 p-4"> {shouldShowModelSelector && (
<div className="space-y-1"> <div className="space-y-2">
<FormLabel> <FormLabel htmlFor="apiFormat">
{t("providerForm.openrouterCompatMode", { {t("providerForm.apiFormat", { defaultValue: "API 格式" })}
defaultValue: "OpenRouter 兼容模式", </FormLabel>
})} <Select value={apiFormat} onValueChange={onApiFormatChange}>
</FormLabel> <SelectTrigger id="apiFormat" className="w-full">
<p className="text-xs text-muted-foreground"> <SelectValue />
{t("providerForm.openrouterCompatModeHint", { </SelectTrigger>
defaultValue: <SelectContent>
"使用 OpenAI Chat Completions 接口并转换为 Anthropic SSE。", <SelectItem value="anthropic">
})} {t("providerForm.apiFormatAnthropic", {
</p> defaultValue: "Anthropic Messages (原生)",
</div> })}
<Switch </SelectItem>
checked={openRouterCompatEnabled} <SelectItem value="openai_chat">
onCheckedChange={onOpenRouterCompatChange} {t("providerForm.apiFormatOpenAIChat", {
/> defaultValue: "OpenAI Chat Completions (需转换)",
})}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("providerForm.apiFormatHint", {
defaultValue: "选择供应商 API 的输入格式",
})}
</p>
</div> </div>
)} )}
@@ -139,6 +139,8 @@ interface OpenCodeFormFieldsProps {
category?: ProviderCategory; category?: ProviderCategory;
shouldShowApiKeyLink: boolean; shouldShowApiKeyLink: boolean;
websiteUrl: string; websiteUrl: string;
isPartner?: boolean;
partnerPromotionKey?: string;
// Base URL // Base URL
baseUrl: string; baseUrl: string;
@@ -161,6 +163,8 @@ export function OpenCodeFormFields({
category, category,
shouldShowApiKeyLink, shouldShowApiKeyLink,
websiteUrl, websiteUrl,
isPartner,
partnerPromotionKey,
baseUrl, baseUrl,
onBaseUrlChange, onBaseUrlChange,
models, models,
@@ -376,6 +380,8 @@ export function OpenCodeFormFields({
category={category} category={category}
shouldShowLink={shouldShowApiKeyLink} shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl} websiteUrl={websiteUrl}
isPartner={isPartner}
partnerPromotionKey={partnerPromotionKey}
/> />
{/* Base URL */} {/* Base URL */}
+43 -52
View File
@@ -15,6 +15,7 @@ import type {
ProviderTestConfig, ProviderTestConfig,
ProviderProxyConfig, ProviderProxyConfig,
CommonConfigEnabledByApp, CommonConfigEnabledByApp,
ClaudeApiFormat,
} from "@/types"; } from "@/types";
import { import {
providerPresets, providerPresets,
@@ -285,8 +286,6 @@ export function ProviderForm({
mode: "onSubmit", mode: "onSubmit",
}); });
const settingsConfigValue = form.getValues("settingsConfig");
// 使用 API Key hook // 使用 API Key hook
const { const {
apiKey, apiKey,
@@ -325,52 +324,16 @@ export function ProviderForm({
onConfigChange: (config) => form.setValue("settingsConfig", config), onConfigChange: (config) => form.setValue("settingsConfig", config),
}); });
const isOpenRouterProvider = useMemo(() => { // Claude API Format state - stored in meta, not settingsConfig
if (appId !== "claude") return false; // Read initial value from meta.apiFormat, default to "anthropic"
const normalized = baseUrl.trim().toLowerCase(); const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
if (normalized.includes("openrouter.ai")) { if (appId !== "claude") return "anthropic";
return true; return initialData?.meta?.apiFormat ?? "anthropic";
} });
try {
const config = JSON.parse(settingsConfigValue || "{}");
const envUrl = config?.env?.ANTHROPIC_BASE_URL;
return typeof envUrl === "string" && envUrl.includes("openrouter.ai");
} catch {
return false;
}
}, [appId, baseUrl, settingsConfigValue]);
const openRouterCompatEnabled = useMemo(() => { const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
if (!isOpenRouterProvider) return false; setLocalApiFormat(format);
try { }, []);
const config = JSON.parse(settingsConfigValue || "{}");
const raw = config?.openrouter_compat_mode;
if (typeof raw === "boolean") return raw;
if (typeof raw === "number") return raw !== 0;
if (typeof raw === "string") {
const normalized = raw.trim().toLowerCase();
return normalized === "true" || normalized === "1";
}
} catch {
// ignore
}
return false; // OpenRouter now supports Claude Code compatible API, no need for transform
}, [isOpenRouterProvider, settingsConfigValue]);
const handleOpenRouterCompatChange = useCallback(
(enabled: boolean) => {
try {
const currentConfig = JSON.parse(
form.getValues("settingsConfig") || "{}",
);
currentConfig.openrouter_compat_mode = enabled;
form.setValue("settingsConfig", JSON.stringify(currentConfig, null, 2));
} catch {
// ignore
}
},
[form],
);
// 使用 Codex 配置 hook (仅 Codex 模式) // 使用 Codex 配置 hook (仅 Codex 模式)
const { const {
@@ -1212,6 +1175,11 @@ export function ProviderForm({
...(shouldPersistCommonConfigEnabledByApp ...(shouldPersistCommonConfigEnabledByApp
? { commonConfigEnabledByApp } ? { commonConfigEnabledByApp }
: {}), : {}),
// Claude API 格式(仅非官方 Claude 供应商使用)
apiFormat:
appId === "claude" && category !== "official"
? localApiFormat
: undefined,
}; };
onSubmit(payload); onSubmit(payload);
@@ -1279,6 +1247,20 @@ export function ProviderForm({
formWebsiteUrl: form.watch("websiteUrl") || "", formWebsiteUrl: form.watch("websiteUrl") || "",
}); });
// 使用 API Key 链接 hook (OpenCode)
const {
shouldShowApiKeyLink: shouldShowOpencodeApiKeyLink,
websiteUrl: opencodeWebsiteUrl,
isPartner: isOpencodePartner,
partnerPromotionKey: opencodePartnerPromotionKey,
} = useApiKeyLink({
appId: "opencode",
category,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
// 使用端点测速候选 hook // 使用端点测速候选 hook
const speedTestEndpoints = useSpeedTestEndpoints({ const speedTestEndpoints = useSpeedTestEndpoints({
appId, appId,
@@ -1408,6 +1390,14 @@ export function ProviderForm({
preset.templateValues, preset.templateValues,
); );
// Sync preset's apiFormat to local state (for Claude providers)
if (preset.apiFormat) {
setLocalApiFormat(preset.apiFormat);
} else {
// Reset to default if preset doesn't specify apiFormat
setLocalApiFormat("anthropic");
}
form.reset({ form.reset({
name: preset.name, name: preset.name,
websiteUrl: preset.websiteUrl ?? "", websiteUrl: preset.websiteUrl ?? "",
@@ -1531,9 +1521,8 @@ export function ProviderForm({
defaultOpusModel={defaultOpusModel} defaultOpusModel={defaultOpusModel}
onModelChange={handleModelChange} onModelChange={handleModelChange}
speedTestEndpoints={speedTestEndpoints} speedTestEndpoints={speedTestEndpoints}
showOpenRouterCompatToggle={false} apiFormat={localApiFormat}
openRouterCompatEnabled={openRouterCompatEnabled} onApiFormatChange={handleApiFormatChange}
onOpenRouterCompatChange={handleOpenRouterCompatChange}
/> />
)} )}
@@ -1603,8 +1592,10 @@ export function ProviderForm({
apiKey={opencodeApiKey} apiKey={opencodeApiKey}
onApiKeyChange={handleOpencodeApiKeyChange} onApiKeyChange={handleOpencodeApiKeyChange}
category={category} category={category}
shouldShowApiKeyLink={false} shouldShowApiKeyLink={shouldShowOpencodeApiKeyLink}
websiteUrl="" websiteUrl={opencodeWebsiteUrl}
isPartner={isOpencodePartner}
partnerPromotionKey={opencodePartnerPromotionKey}
baseUrl={opencodeBaseUrl} baseUrl={opencodeBaseUrl}
onBaseUrlChange={handleOpencodeBaseUrlChange} onBaseUrlChange={handleOpencodeBaseUrlChange}
models={opencodeModels} models={opencodeModels}
@@ -4,10 +4,15 @@ import type { ProviderCategory } from "@/types";
import type { ProviderPreset } from "@/config/claudeProviderPresets"; import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets"; import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets"; import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
import type { OpenCodeProviderPreset } from "@/config/opencodeProviderPresets";
type PresetEntry = { type PresetEntry = {
id: string; id: string;
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset; preset:
| ProviderPreset
| CodexProviderPreset
| GeminiProviderPreset
| OpenCodeProviderPreset;
}; };
interface UseApiKeyLinkProps { interface UseApiKeyLinkProps {
@@ -74,7 +79,10 @@ export function useApiKeyLink({
return { return {
shouldShowApiKeyLink: shouldShowApiKeyLink:
appId === "claude" || appId === "codex" || appId === "gemini" appId === "claude" ||
appId === "codex" ||
appId === "gemini" ||
appId === "opencode"
? shouldShowApiKeyLink ? shouldShowApiKeyLink
: false, : false,
websiteUrl: getWebsiteUrl, websiteUrl: getWebsiteUrl,
@@ -4,6 +4,7 @@ import type { AppId } from "@/lib/api";
import { providerPresets } from "@/config/claudeProviderPresets"; import { providerPresets } from "@/config/claudeProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets"; import { codexProviderPresets } from "@/config/codexProviderPresets";
import { geminiProviderPresets } from "@/config/geminiProviderPresets"; import { geminiProviderPresets } from "@/config/geminiProviderPresets";
import { opencodeProviderPresets } from "@/config/opencodeProviderPresets";
interface UseProviderCategoryProps { interface UseProviderCategoryProps {
appId: AppId; appId: AppId;
@@ -42,7 +43,9 @@ export function useProviderCategory({
if (!selectedPresetId) return; if (!selectedPresetId) return;
// 从预设 ID 提取索引 // 从预设 ID 提取索引
const match = selectedPresetId.match(/^(claude|codex|gemini)-(\d+)$/); const match = selectedPresetId.match(
/^(claude|codex|gemini|opencode)-(\d+)$/,
);
if (!match) return; if (!match) return;
const [, type, indexStr] = match; const [, type, indexStr] = match;
@@ -67,6 +70,11 @@ export function useProviderCategory({
if (preset) { if (preset) {
setCategory(preset.category || undefined); setCategory(preset.category || undefined);
} }
} else if (type === "opencode" && appId === "opencode") {
const preset = opencodeProviderPresets[index];
if (preset) {
setCategory(preset.category || undefined);
}
} }
}, [appId, selectedPresetId, isEditMode, initialCategory]); }, [appId, selectedPresetId, isEditMode, initialCategory]);
+11 -7
View File
@@ -37,7 +37,9 @@ curl -fsSL https://claude.ai/install.sh | bash
# Codex # Codex
npm i -g @openai/codex@latest npm i -g @openai/codex@latest
# Gemini CLI # Gemini CLI
npm i -g @google/gemini-cli@latest`; npm i -g @google/gemini-cli@latest
# OpenCode
curl -fsSL https://opencode.ai/install | bash`;
export function AboutSection({ isPortable }: AboutSectionProps) { export function AboutSection({ isPortable }: AboutSectionProps) {
// ... (use hooks as before) ... // ... (use hooks as before) ...
@@ -315,10 +317,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
{isLoadingTools ? t("common.refreshing") : t("common.refresh")} {isLoadingTools ? t("common.refreshing") : t("common.refresh")}
</Button> </Button>
</div> </div>
<div className="grid gap-3 sm:grid-cols-3 px-1"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 px-1">
{["claude", "codex", "gemini"].map((toolName, index) => { {["claude", "codex", "gemini", "opencode"].map((toolName, index) => {
const tool = toolVersions.find((item) => item.name === toolName); const tool = toolVersions.find((item) => item.name === toolName);
const displayName = tool?.name ?? toolName; // Special case for OpenCode (capital C), others use capitalize
const displayName =
toolName === "opencode"
? "OpenCode"
: toolName.charAt(0).toUpperCase() + toolName.slice(1);
const title = tool?.version || tool?.error || t("common.unknown"); const title = tool?.version || tool?.error || t("common.unknown");
return ( return (
@@ -333,9 +339,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Terminal className="h-4 w-4 text-muted-foreground" /> <Terminal className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium capitalize"> <span className="text-sm font-medium">{displayName}</span>
{displayName}
</span>
</div> </div>
{isLoadingTools ? ( {isLoadingTools ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+44
View File
@@ -43,6 +43,11 @@ export interface ProviderPreset {
// 图标配置 // 图标配置
icon?: string; // 图标名称 icon?: string; // 图标名称
iconColor?: string; // 图标颜色 iconColor?: string; // 图标颜色
// Claude API 格式(仅 Claude 供应商使用)
// - "anthropic" (默认): Anthropic Messages API 格式,直接透传
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
apiFormat?: "anthropic" | "openai_chat";
} }
export const providerPresets: ProviderPreset[] = [ export const providerPresets: ProviderPreset[] = [
@@ -461,6 +466,26 @@ export const providerPresets: ProviderPreset[] = [
icon: "rc", icon: "rc",
iconColor: "#E96B2C", iconColor: "#E96B2C",
}, },
{
name: "AICodeMirror",
websiteUrl: "https://www.aicodemirror.com",
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.aicodemirror.com/api/claudecode",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: [
"https://api.aicodemirror.com/api/claudecode",
"https://api.claudecode.net.cn/api/claudecode",
],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "aicodemirror", // 促销信息 i18n key
icon: "aicodemirror",
iconColor: "#000000",
},
{ {
name: "OpenRouter", name: "OpenRouter",
websiteUrl: "https://openrouter.ai", websiteUrl: "https://openrouter.ai",
@@ -479,6 +504,25 @@ export const providerPresets: ProviderPreset[] = [
icon: "openrouter", icon: "openrouter",
iconColor: "#6566F1", iconColor: "#6566F1",
}, },
{
name: "Nvidia",
websiteUrl: "https://build.nvidia.com",
apiKeyUrl: "https://build.nvidia.com/settings/api-keys",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://integrate.api.nvidia.com",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "moonshotai/kimi-k2.5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "moonshotai/kimi-k2.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k2.5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k2.5",
},
},
category: "aggregator",
apiFormat: "openai_chat",
icon: "nvidia",
iconColor: "#000000",
},
{ {
name: "Xiaomi MiMo", name: "Xiaomi MiMo",
websiteUrl: "https://platform.xiaomimimo.com", websiteUrl: "https://platform.xiaomimimo.com",
+19
View File
@@ -208,6 +208,25 @@ requires_openai_auth = true`,
icon: "rc", icon: "rc",
iconColor: "#E96B2C", iconColor: "#E96B2C",
}, },
{
name: "AICodeMirror",
websiteUrl: "https://www.aicodemirror.com",
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"aicodemirror",
"https://api.aicodemirror.com/api/codex/backend-api/codex",
"gpt-5.2",
),
endpointCandidates: [
"https://api.aicodemirror.com/api/codex/backend-api/codex",
"https://api.claudecode.net.cn/api/codex/backend-api/codex",
],
isPartner: true,
partnerPromotionKey: "aicodemirror",
icon: "aicodemirror",
iconColor: "#000000",
},
{ {
name: "OpenRouter", name: "OpenRouter",
websiteUrl: "https://openrouter.ai", websiteUrl: "https://openrouter.ai",
+23
View File
@@ -116,6 +116,29 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
icon: "aigocode", icon: "aigocode",
iconColor: "#5B7FFF", iconColor: "#5B7FFF",
}, },
{
name: "AICodeMirror",
websiteUrl: "https://www.aicodemirror.com",
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.aicodemirror.com/api/gemini",
GEMINI_MODEL: "gemini-3-pro",
},
},
baseURL: "https://api.aicodemirror.com/api/gemini",
model: "gemini-3-pro",
description: "AICodeMirror",
category: "third_party",
isPartner: true,
partnerPromotionKey: "aicodemirror",
endpointCandidates: [
"https://api.aicodemirror.com/api/gemini",
"https://api.claudecode.net.cn/api/gemini",
],
icon: "aicodemirror",
iconColor: "#000000",
},
{ {
name: "OpenRouter", name: "OpenRouter",
websiteUrl: "https://openrouter.ai", websiteUrl: "https://openrouter.ai",
+55
View File
@@ -558,6 +558,32 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
}, },
}, },
}, },
{
name: "Nvidia",
websiteUrl: "https://build.nvidia.com",
apiKeyUrl: "https://build.nvidia.com/settings/api-keys",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "Nvidia",
options: {
baseURL: "https://integrate.api.nvidia.com/v1",
apiKey: "",
},
models: {
"moonshotai/kimi-k2.5": { name: "Kimi K2.5" },
},
},
category: "aggregator",
icon: "nvidia",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
// ========== 第三方合作伙伴 ========== // ========== 第三方合作伙伴 ==========
{ {
@@ -675,6 +701,35 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
}, },
}, },
}, },
{
name: "AICodeMirror",
websiteUrl: "https://www.aicodemirror.com",
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "AICodeMirror",
options: {
baseURL: "https://api.aicodemirror.com/api/claudecode",
apiKey: "",
},
models: {
"claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
"claude-opus-4.5": { name: "Claude Opus 4.5" },
},
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "aicodemirror",
icon: "aicodemirror",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
// ========== 自定义模板 ========== // ========== 自定义模板 ==========
{ {
+34 -2
View File
@@ -84,11 +84,43 @@ export function useProviderActions(activeApp: AppId) {
try { try {
await switchProviderMutation.mutateAsync(provider.id); await switchProviderMutation.mutateAsync(provider.id);
await syncClaudePlugin(provider); await syncClaudePlugin(provider);
// 根据供应商类型显示不同的成功提示
if (
activeApp === "claude" &&
provider.category !== "official" &&
provider.meta?.apiFormat === "openai_chat"
) {
// OpenAI Chat 格式供应商:显示代理提示
toast.info(
t("notifications.openAIChatFormatHint", {
defaultValue:
"此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
}),
{
duration: 5000,
closeButton: true,
},
);
} else {
// 普通供应商:显示切换成功
// OpenCode: show "added to config" message instead of "switched"
const messageKey =
activeApp === "opencode"
? "notifications.addToConfigSuccess"
: "notifications.switchSuccess";
const defaultMessage =
activeApp === "opencode" ? "已添加到配置" : "切换成功!";
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
closeButton: true,
});
}
} catch { } catch {
// 错误提示由 mutation 与同步函数处理 // 错误提示由 mutation 处理
} }
}, },
[switchProviderMutation, syncClaudePlugin], [switchProviderMutation, syncClaudePlugin, activeApp, t],
); );
// 删除供应商 // 删除供应商
+9 -5
View File
@@ -163,7 +163,8 @@
"deleteSuccess": "Provider deleted", "deleteSuccess": "Provider deleted",
"deleteFailed": "Failed to delete provider: {{error}}", "deleteFailed": "Failed to delete provider: {{error}}",
"settingsSaved": "Settings saved", "settingsSaved": "Settings saved",
"settingsSaveFailed": "Failed to save settings: {{error}}" "settingsSaveFailed": "Failed to save settings: {{error}}",
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled"
}, },
"confirm": { "confirm": {
"deleteProvider": "Delete Provider", "deleteProvider": "Delete Provider",
@@ -359,7 +360,7 @@
"viewReleaseNotes": "View release notes for this version", "viewReleaseNotes": "View release notes for this version",
"viewCurrentReleaseNotes": "View current version release notes", "viewCurrentReleaseNotes": "View current version release notes",
"oneClickInstall": "One-click Install", "oneClickInstall": "One-click Install",
"oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI", "oneClickInstallHint": "Install Claude Code / Codex / Gemini CLI / OpenCode",
"localEnvCheck": "Local environment check", "localEnvCheck": "Local environment check",
"installCommandsCopied": "Install commands copied", "installCommandsCopied": "Install commands copied",
"installCommandsCopyFailed": "Copy failed, please copy manually.", "installCommandsCopyFailed": "Copy failed, please copy manually.",
@@ -455,7 +456,8 @@
"dmxapi": "Claude Code exclusive model 66% OFF now!", "dmxapi": "Claude Code exclusive model 66% OFF now!",
"cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up", "cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up",
"aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!", "aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!",
"rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!" "rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!",
"aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!"
}, },
"parameterConfig": "Parameter Config - {{name}} *", "parameterConfig": "Parameter Config - {{name}} *",
"mainModel": "Main Model (optional)", "mainModel": "Main Model (optional)",
@@ -480,8 +482,10 @@
"anthropicModel": "Main Model", "anthropicModel": "Main Model",
"anthropicSmallFastModel": "Fast Model", "anthropicSmallFastModel": "Fast Model",
"anthropicReasoningModel": "Reasoning Model (Thinking)", "anthropicReasoningModel": "Reasoning Model (Thinking)",
"openrouterCompatMode": "OpenRouter Compatibility Mode", "apiFormat": "API Format",
"openrouterCompatModeHint": "Use OpenAI Chat Completions interface and convert to Anthropic SSE.", "apiFormatHint": "Select the input format for the provider's API",
"apiFormatAnthropic": "Anthropic Messages (Native)",
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
"anthropicDefaultHaikuModel": "Default Haiku Model", "anthropicDefaultHaikuModel": "Default Haiku Model",
"anthropicDefaultSonnetModel": "Default Sonnet Model", "anthropicDefaultSonnetModel": "Default Sonnet Model",
"anthropicDefaultOpusModel": "Default Opus Model", "anthropicDefaultOpusModel": "Default Opus Model",
+9 -5
View File
@@ -163,7 +163,8 @@
"deleteSuccess": "プロバイダーを削除しました", "deleteSuccess": "プロバイダーを削除しました",
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}", "deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
"settingsSaved": "設定を保存しました", "settingsSaved": "設定を保存しました",
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}" "settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です"
}, },
"confirm": { "confirm": {
"deleteProvider": "プロバイダーを削除", "deleteProvider": "プロバイダーを削除",
@@ -359,7 +360,7 @@
"viewReleaseNotes": "このバージョンのリリースノートを見る", "viewReleaseNotes": "このバージョンのリリースノートを見る",
"viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る", "viewCurrentReleaseNotes": "現在のバージョンのリリースノートを見る",
"oneClickInstall": "ワンクリックインストール", "oneClickInstall": "ワンクリックインストール",
"oneClickInstallHint": "Claude Code / Codex / Gemini CLI をインストール", "oneClickInstallHint": "Claude Code / Codex / Gemini CLI / OpenCode をインストール",
"localEnvCheck": "ローカル環境チェック", "localEnvCheck": "ローカル環境チェック",
"installCommandsCopied": "インストールコマンドをコピーしました", "installCommandsCopied": "インストールコマンドをコピーしました",
"installCommandsCopyFailed": "コピーに失敗しました。手動でコピーしてください。", "installCommandsCopyFailed": "コピーに失敗しました。手動でコピーしてください。",
@@ -455,7 +456,8 @@
"dmxapi": "Claude Code 専用モデル 66% OFF 実施中!", "dmxapi": "Claude Code 専用モデル 66% OFF 実施中!",
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ", "cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!", "aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!" "rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!",
"aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!"
}, },
"parameterConfig": "パラメーター設定 - {{name}} *", "parameterConfig": "パラメーター設定 - {{name}} *",
"mainModel": "メインモデル(任意)", "mainModel": "メインモデル(任意)",
@@ -480,8 +482,10 @@
"anthropicModel": "メインモデル", "anthropicModel": "メインモデル",
"anthropicSmallFastModel": "高速モデル", "anthropicSmallFastModel": "高速モデル",
"anthropicReasoningModel": "推論モデル(Thinking", "anthropicReasoningModel": "推論モデル(Thinking",
"openrouterCompatMode": "OpenRouter 互換モード", "apiFormat": "API フォーマット",
"openrouterCompatModeHint": "OpenAI Chat Completions インターフェースを使用し、Anthropic SSE に変換します。", "apiFormatHint": "プロバイダー API の入力フォーマットを選択",
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
"anthropicDefaultHaikuModel": "既定 Haiku モデル", "anthropicDefaultHaikuModel": "既定 Haiku モデル",
"anthropicDefaultSonnetModel": "既定 Sonnet モデル", "anthropicDefaultSonnetModel": "既定 Sonnet モデル",
"anthropicDefaultOpusModel": "既定 Opus モデル", "anthropicDefaultOpusModel": "既定 Opus モデル",
+9 -5
View File
@@ -163,7 +163,8 @@
"deleteSuccess": "供应商已删除", "deleteSuccess": "供应商已删除",
"deleteFailed": "删除供应商失败:{{error}}", "deleteFailed": "删除供应商失败:{{error}}",
"settingsSaved": "设置已保存", "settingsSaved": "设置已保存",
"settingsSaveFailed": "保存设置失败:{{error}}" "settingsSaveFailed": "保存设置失败:{{error}}",
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用"
}, },
"confirm": { "confirm": {
"deleteProvider": "删除供应商", "deleteProvider": "删除供应商",
@@ -359,7 +360,7 @@
"viewReleaseNotes": "查看该版本更新日志", "viewReleaseNotes": "查看该版本更新日志",
"viewCurrentReleaseNotes": "查看当前版本更新日志", "viewCurrentReleaseNotes": "查看当前版本更新日志",
"oneClickInstall": "一键安装", "oneClickInstall": "一键安装",
"oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI", "oneClickInstallHint": "安装 Claude Code / Codex / Gemini CLI / OpenCode",
"localEnvCheck": "本地环境检查", "localEnvCheck": "本地环境检查",
"installCommandsCopied": "安装命令已复制", "installCommandsCopied": "安装命令已复制",
"installCommandsCopyFailed": "复制失败,请手动复制。", "installCommandsCopyFailed": "复制失败,请手动复制。",
@@ -455,7 +456,8 @@
"dmxapi": "Claude Code 专属模型 3.4 折优惠进行中!", "dmxapi": "Claude Code 专属模型 3.4 折优惠进行中!",
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠", "cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!", "aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!" "rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!",
"aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!"
}, },
"parameterConfig": "参数配置 - {{name}} *", "parameterConfig": "参数配置 - {{name}} *",
"mainModel": "主模型 (可选)", "mainModel": "主模型 (可选)",
@@ -480,8 +482,10 @@
"anthropicModel": "主模型", "anthropicModel": "主模型",
"anthropicSmallFastModel": "快速模型", "anthropicSmallFastModel": "快速模型",
"anthropicReasoningModel": "推理模型 (Thinking)", "anthropicReasoningModel": "推理模型 (Thinking)",
"openrouterCompatMode": "OpenRouter 兼容模式", "apiFormat": "API 格式",
"openrouterCompatModeHint": "使用 OpenAI Chat Completions 接口并转换为 Anthropic SSE。", "apiFormatHint": "选择供应商 API 的输入格式",
"apiFormatAnthropic": "Anthropic Messages (原生)",
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
"anthropicDefaultHaikuModel": "Haiku 默认模型", "anthropicDefaultHaikuModel": "Haiku 默认模型",
"anthropicDefaultSonnetModel": "Sonnet 默认模型", "anthropicDefaultSonnetModel": "Sonnet 默认模型",
"anthropicDefaultOpusModel": "Opus 默认模型", "anthropicDefaultOpusModel": "Opus 默认模型",
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1209px" height="1255px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 1017.97 1056.47"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#E4906E;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="圖層_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+2
View File
@@ -2,6 +2,7 @@
// Do not edit manually // Do not edit manually
export const icons: Record<string, string> = { export const icons: Record<string, string> = {
aicodemirror: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 1017.97 1056.47"><title>AICodeMirror</title><path fill="#E4906E" fill-rule="nonzero" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/></svg>`,
aigocode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 648 564" xmlns="http://www.w3.org/2000/svg"><title>AiGoCode</title><g transform="translate(0,564) scale(0.1,-0.1)"><path fill="#7C6AEF" d="M5392 5379 c-26 -10 -36 -28 -56 -108 -24 -94 -47 -140 -101 -199 -56 -62 -117 -96 -219 -121 -101 -25 -116 -35 -116 -75 0 -45 21 -60 123 -89 190 -53 271 -147 336 -384 13 -46 38 -61 85 -49 25 6 36 28 56 111 7 28 23 72 36 99 13 28 21 54 18 58 -3 5 -2 7 3 6 4 -2 29 18 55 44 41 41 145 110 173 114 56 8 126 27 131 36 4 6 7 30 8 54 1 49 -5 54 -104 74 -164 33 -280 148 -320 320 -23 101 -52 129 -108 109z"/><path fill="#5B7FFF" d="M1770 4814 c-138 -25 -301 -93 -425 -177 -80 -55 -227 -197 -280 -272 -98 -138 -161 -279 -201 -447 -16 -66 -18 -164 -21 -1098 -4 -1130 -4 -1144 57 -1331 80 -242 222 -434 444 -598 l56 -42 0 -337 c0 -309 2 -340 19 -379 31 -66 87 -93 158 -74 22 6 202 117 404 249 200 131 398 259 439 285 144 89 48 81 1095 88 912 5 932 6 1012 27 243 64 441 178 599 344 166 176 274 408 304 648 13 110 13 2016 0 2120 -25 190 -83 347 -183 498 -160 240 -384 400 -677 484 l-75 22 -1325 2 c-1086 2 -1339 0 -1400 -12z m1068 -1455 l-3 -751 -71 -18 c-71 -18 -154 -55 -205 -91 -14 -10 -29 -16 -33 -12 -3 3 -6 91 -6 195 l0 188 -306 0 -305 0 -21 -47 c-11 -27 -33 -75 -48 -108 -16 -33 -44 -96 -62 -140 l-34 -80 -172 -3 c-101 -1 -172 1 -172 7 0 5 14 40 31 78 31 68 101 227 254 578 335 769 358 814 434 874 94 74 122 79 449 80 l272 1 -2 -751z m794 717 c41 -13 103 -42 139 -62 67 -40 202 -168 232 -221 l17 -31 -77 -65 c-43 -35 -101 -80 -129 -101 l-52 -36 -39 57 c-79 116 -204 177 -313 154 -66 -14 -105 -42 -130 -91 -19 -37 -20 -60 -20 -400 0 -339 1 -363 20 -399 26 -51 61 -78 128 -97 143 -42 327 52 366 186 l13 45 -163 3 -163 2 -3 157 c-2 111 0 157 9 160 6 2 263 3 570 1 487 -3 562 -5 593 -19 98 -45 125 -159 58 -244 -39 -50 -66 -55 -322 -55 l-236 0 -6 -27 c-18 -83 -29 -115 -62 -183 -69 -143 -230 -269 -402 -316 -81 -22 -271 -25 -350 -5 -173 43 -289 145 -341 298 -19 57 -20 81 -17 534 l3 474 33 67 c55 112 168 199 307 235 79 20 246 10 337 -21z m828 -1515 c67 -71 67 -73 -62 -396 -45 -110 -102 -254 -128 -320 -254 -639 -272 -681 -298 -702 -74 -62 -192 -15 -192 77 0 12 39 116 86 233 48 117 97 239 110 272 119 311 336 833 354 852 36 37 85 32 130 -16z m-1574 -205 c16 -13 19 -29 22 -128 l3 -113 -195 -155 c-108 -85 -196 -158 -196 -161 0 -4 17 -19 38 -35 128 -96 327 -272 339 -301 16 -39 17 -143 2 -177 -15 -33 -34 -39 -67 -22 -48 26 -566 446 -584 474 -23 35 -23 89 1 128 16 27 150 143 376 326 39 31 79 65 90 75 44 41 128 103 139 103 7 0 21 -6 32 -14z m713 -25 c52 -37 53 -33 -92 -551 -36 -129 -80 -289 -98 -355 -39 -143 -62 -175 -129 -175 -47 0 -92 20 -114 52 -24 34 -19 90 18 217 19 64 78 271 131 461 53 190 98 353 101 364 6 16 14 18 79 14 54 -4 81 -11 104 -27z m1116 -351 c55 -45 117 -98 138 -120 61 -64 48 -115 -50 -192 -32 -25 -118 -94 -191 -152 -136 -108 -163 -120 -217 -100 -26 10 -47 62 -39 97 10 46 22 62 94 119 36 29 93 77 128 106 l62 55 -65 59 c-71 65 -77 84 -49 141 24 47 44 67 68 67 12 0 66 -36 121 -80z"/><path fill="#5B7FFF" d="M2328 3755 c-37 -20 -54 -53 -153 -280 -48 -110 -96 -219 -106 -242 l-18 -43 234 0 235 0 0 290 0 290 -82 0 c-53 -1 -93 -6 -110 -15z"/></g></svg>`, aigocode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 648 564" xmlns="http://www.w3.org/2000/svg"><title>AiGoCode</title><g transform="translate(0,564) scale(0.1,-0.1)"><path fill="#7C6AEF" d="M5392 5379 c-26 -10 -36 -28 -56 -108 -24 -94 -47 -140 -101 -199 -56 -62 -117 -96 -219 -121 -101 -25 -116 -35 -116 -75 0 -45 21 -60 123 -89 190 -53 271 -147 336 -384 13 -46 38 -61 85 -49 25 6 36 28 56 111 7 28 23 72 36 99 13 28 21 54 18 58 -3 5 -2 7 3 6 4 -2 29 18 55 44 41 41 145 110 173 114 56 8 126 27 131 36 4 6 7 30 8 54 1 49 -5 54 -104 74 -164 33 -280 148 -320 320 -23 101 -52 129 -108 109z"/><path fill="#5B7FFF" d="M1770 4814 c-138 -25 -301 -93 -425 -177 -80 -55 -227 -197 -280 -272 -98 -138 -161 -279 -201 -447 -16 -66 -18 -164 -21 -1098 -4 -1130 -4 -1144 57 -1331 80 -242 222 -434 444 -598 l56 -42 0 -337 c0 -309 2 -340 19 -379 31 -66 87 -93 158 -74 22 6 202 117 404 249 200 131 398 259 439 285 144 89 48 81 1095 88 912 5 932 6 1012 27 243 64 441 178 599 344 166 176 274 408 304 648 13 110 13 2016 0 2120 -25 190 -83 347 -183 498 -160 240 -384 400 -677 484 l-75 22 -1325 2 c-1086 2 -1339 0 -1400 -12z m1068 -1455 l-3 -751 -71 -18 c-71 -18 -154 -55 -205 -91 -14 -10 -29 -16 -33 -12 -3 3 -6 91 -6 195 l0 188 -306 0 -305 0 -21 -47 c-11 -27 -33 -75 -48 -108 -16 -33 -44 -96 -62 -140 l-34 -80 -172 -3 c-101 -1 -172 1 -172 7 0 5 14 40 31 78 31 68 101 227 254 578 335 769 358 814 434 874 94 74 122 79 449 80 l272 1 -2 -751z m794 717 c41 -13 103 -42 139 -62 67 -40 202 -168 232 -221 l17 -31 -77 -65 c-43 -35 -101 -80 -129 -101 l-52 -36 -39 57 c-79 116 -204 177 -313 154 -66 -14 -105 -42 -130 -91 -19 -37 -20 -60 -20 -400 0 -339 1 -363 20 -399 26 -51 61 -78 128 -97 143 -42 327 52 366 186 l13 45 -163 3 -163 2 -3 157 c-2 111 0 157 9 160 6 2 263 3 570 1 487 -3 562 -5 593 -19 98 -45 125 -159 58 -244 -39 -50 -66 -55 -322 -55 l-236 0 -6 -27 c-18 -83 -29 -115 -62 -183 -69 -143 -230 -269 -402 -316 -81 -22 -271 -25 -350 -5 -173 43 -289 145 -341 298 -19 57 -20 81 -17 534 l3 474 33 67 c55 112 168 199 307 235 79 20 246 10 337 -21z m828 -1515 c67 -71 67 -73 -62 -396 -45 -110 -102 -254 -128 -320 -254 -639 -272 -681 -298 -702 -74 -62 -192 -15 -192 77 0 12 39 116 86 233 48 117 97 239 110 272 119 311 336 833 354 852 36 37 85 32 130 -16z m-1574 -205 c16 -13 19 -29 22 -128 l3 -113 -195 -155 c-108 -85 -196 -158 -196 -161 0 -4 17 -19 38 -35 128 -96 327 -272 339 -301 16 -39 17 -143 2 -177 -15 -33 -34 -39 -67 -22 -48 26 -566 446 -584 474 -23 35 -23 89 1 128 16 27 150 143 376 326 39 31 79 65 90 75 44 41 128 103 139 103 7 0 21 -6 32 -14z m713 -25 c52 -37 53 -33 -92 -551 -36 -129 -80 -289 -98 -355 -39 -143 -62 -175 -129 -175 -47 0 -92 20 -114 52 -24 34 -19 90 18 217 19 64 78 271 131 461 53 190 98 353 101 364 6 16 14 18 79 14 54 -4 81 -11 104 -27z m1116 -351 c55 -45 117 -98 138 -120 61 -64 48 -115 -50 -192 -32 -25 -118 -94 -191 -152 -136 -108 -163 -120 -217 -100 -26 10 -47 62 -39 97 10 46 22 62 94 119 36 29 93 77 128 106 l62 55 -65 59 c-71 65 -77 84 -49 141 24 47 44 67 68 67 12 0 66 -36 121 -80z"/><path fill="#5B7FFF" d="M2328 3755 c-37 -20 -54 -53 -153 -280 -48 -110 -96 -219 -106 -242 l-18 -43 234 0 235 0 0 290 0 290 -82 0 c-53 -1 -93 -6 -110 -15z"/></g></svg>`,
alibaba: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>`, alibaba: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>`,
anthropic: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>`, anthropic: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>`,
@@ -58,6 +59,7 @@ export const icons: Record<string, string> = {
siliconflow: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>`, siliconflow: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>`,
catcoder: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>`, catcoder: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>`,
mcp: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>`, mcp: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>`,
nvidia: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>`,
}; };
export const iconList = Object.keys(icons); export const iconList = Object.keys(icons);
+7
View File
@@ -359,6 +359,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["xiaomimimo", "xiaomi", "mimo"], keywords: ["xiaomimimo", "xiaomi", "mimo"],
defaultColor: "#000000", defaultColor: "#000000",
}, },
nvidia: {
name: "nvidia",
displayName: "NVIDIA",
category: "ai-provider",
keywords: ["nvidia", "nim", "gpu"],
defaultColor: "#74B71B",
},
}; };
export function getIconMetadata(name: string): IconMetadata | undefined { export function getIconMetadata(name: string): IconMetadata | undefined {
+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+8 -14
View File
@@ -61,10 +61,11 @@ export const useAddProviderMutation = (appId: AppId) => {
); );
}, },
onError: (error: Error) => { onError: (error: Error) => {
const detail = extractErrorMessage(error) || t("common.unknown");
toast.error( toast.error(
t("notifications.addFailed", { t("notifications.addFailed", {
defaultValue: "添加供应商失败: {{error}}", defaultValue: "添加供应商失败: {{error}}",
error: error.message, error: detail,
}), }),
); );
}, },
@@ -92,10 +93,11 @@ export const useUpdateProviderMutation = (appId: AppId) => {
); );
}, },
onError: (error: Error) => { onError: (error: Error) => {
const detail = extractErrorMessage(error) || t("common.unknown");
toast.error( toast.error(
t("notifications.updateFailed", { t("notifications.updateFailed", {
defaultValue: "更新供应商失败: {{error}}", defaultValue: "更新供应商失败: {{error}}",
error: error.message, error: detail,
}), }),
); );
}, },
@@ -133,10 +135,11 @@ export const useDeleteProviderMutation = (appId: AppId) => {
); );
}, },
onError: (error: Error) => { onError: (error: Error) => {
const detail = extractErrorMessage(error) || t("common.unknown");
toast.error( toast.error(
t("notifications.deleteFailed", { t("notifications.deleteFailed", {
defaultValue: "删除供应商失败: {{error}}", defaultValue: "删除供应商失败: {{error}}",
error: error.message, error: detail,
}), }),
); );
}, },
@@ -171,17 +174,8 @@ export const useSwitchProviderMutation = (appId: AppId) => {
); );
} }
// OpenCode: show "added to config" message instead of "switched" // Note: Success toast is handled by useProviderActions.switchProvider
const messageKey = // to allow customization based on provider properties (e.g., apiFormat)
appId === "opencode"
? "notifications.addToConfigSuccess"
: "notifications.switchSuccess";
const defaultMessage =
appId === "opencode" ? "已添加到配置" : "切换供应商成功";
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
closeButton: true,
});
}, },
onError: (error: Error) => { onError: (error: Error) => {
const detail = extractErrorMessage(error) || t("common.unknown"); const detail = extractErrorMessage(error) || t("common.unknown");
+9
View File
@@ -149,11 +149,20 @@ export interface ProviderMeta {
commonConfigEnabled?: boolean; commonConfigEnabled?: boolean;
// 按应用记录通用配置启用状态(优先于 commonConfigEnabled // 按应用记录通用配置启用状态(优先于 commonConfigEnabled
commonConfigEnabledByApp?: CommonConfigEnabledByApp; commonConfigEnabledByApp?: CommonConfigEnabledByApp;
// Claude API 格式(仅 Claude 供应商使用)
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
apiFormat?: "anthropic" | "openai_chat";
} }
// Skill 同步方式 // Skill 同步方式
export type SkillSyncMethod = "auto" | "symlink" | "copy"; export type SkillSyncMethod = "auto" | "symlink" | "copy";
// Claude API 格式类型
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
export type ClaudeApiFormat = "anthropic" | "openai_chat";
// 主页面显示的应用配置 // 主页面显示的应用配置
export interface VisibleApps { export interface VisibleApps {
claude: boolean; claude: boolean;