diff --git a/package.json b/package.json index d14bdda6d..1e19c392d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.2.0", + "version": "0.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 98dc63bc1..7acba54d2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2789,7 +2789,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.2.0" +version = "0.3.0" dependencies = [ "async-stream", "axum", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6c663a013..4eb716327 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.2.0" +version = "0.3.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 476079478..8f30c5a3c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -648,6 +648,36 @@ struct TestResult { time_ms: u64, } +#[derive(serde::Serialize)] +struct ModelInfo { + id: String, + object: String, + owned_by: String, +} + +#[tauri::command] +async fn get_available_models() -> Result, String> { + Ok(vec![ + // Kiro/Claude models + ModelInfo { id: "claude-sonnet-4-5".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-sonnet-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-sonnet-4-5-20250929".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-3-7-sonnet-20250219".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-3-5-sonnet-latest".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-opus-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { id: "claude-haiku-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + // Gemini models + ModelInfo { id: "gemini-2.5-flash".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + ModelInfo { id: "gemini-2.5-flash-lite".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + ModelInfo { id: "gemini-2.5-pro".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + ModelInfo { id: "gemini-2.5-pro-preview-06-05".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + ModelInfo { id: "gemini-3-pro-preview".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + // Qwen models + ModelInfo { id: "qwen3-coder-plus".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() }, + ModelInfo { id: "qwen3-coder-flash".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() }, + ]) +} + #[tauri::command] async fn test_api( state: tauri::State<'_, AppState>, @@ -746,6 +776,7 @@ pub fn run() { get_logs, clear_logs, test_api, + get_available_models, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 797859980..01273f79b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.2.0", + "version": "0.3.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 82bb7225c..9b29a3c22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,10 +2,11 @@ import { useState } from "react"; import { Sidebar } from "./components/Sidebar"; import { Dashboard } from "./components/Dashboard"; import { Providers } from "./components/Providers"; +import { Models } from "./components/Models"; import { Settings } from "./components/Settings"; import { Logs } from "./components/Logs"; -type Page = "dashboard" | "providers" | "settings" | "logs"; +type Page = "dashboard" | "providers" | "models" | "settings" | "logs"; function App() { const [currentPage, setCurrentPage] = useState("dashboard"); @@ -16,6 +17,8 @@ function App() { return ; case "providers": return ; + case "models": + return ; case "settings": return ; case "logs": diff --git a/src/components/Models.tsx b/src/components/Models.tsx new file mode 100644 index 000000000..987f31975 --- /dev/null +++ b/src/components/Models.tsx @@ -0,0 +1,261 @@ +import { useState, useEffect } from "react"; +import { Cpu, RefreshCw, Copy, Check, Search } from "lucide-react"; +import { getAvailableModels, ModelInfo } from "@/hooks/useTauri"; + +// 模型分组配置 +const MODEL_GROUPS: Record = { + kiro: { + name: "Kiro Claude", + color: "bg-purple-100 text-purple-700", + models: [ + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250514", + "claude-sonnet-4-5-20250929", + "claude-3-7-sonnet-20250219", + "claude-3-5-sonnet-latest", + "claude-3-5-sonnet-20241022", + "claude-opus-4-5-20250514", + "claude-haiku-4-5-20250514", + ], + }, + gemini: { + name: "Gemini CLI", + color: "bg-blue-100 text-blue-700", + models: [ + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.5-pro", + "gemini-2.5-pro-preview-06-05", + "gemini-3-pro-preview", + "gemini-2.0-flash-exp", + ], + }, + qwen: { + name: "通义千问", + color: "bg-orange-100 text-orange-700", + models: [ + "qwen3-coder-plus", + "qwen3-coder-flash", + "qwen-coder-plus", + "qwen-coder-turbo", + ], + }, + openai: { + name: "OpenAI", + color: "bg-green-100 text-green-700", + models: [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4-turbo", + "gpt-4", + "gpt-3.5-turbo", + "o1-preview", + "o1-mini", + ], + }, +}; + +export function Models() { + const [models, setModels] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(null); + const [search, setSearch] = useState(""); + const [selectedGroup, setSelectedGroup] = useState(null); + + useEffect(() => { + fetchModels(); + }, []); + + const fetchModels = async () => { + setLoading(true); + setError(null); + + try { + const data = await getAvailableModels(); + setModels(data || []); + } catch (e: any) { + setError(e.toString()); + setModels([]); + } + + setLoading(false); + }; + + const copyModelId = (id: string) => { + navigator.clipboard.writeText(id); + setCopied(id); + setTimeout(() => setCopied(null), 2000); + }; + + const getModelGroup = (modelId: string): string | null => { + for (const [groupId, group] of Object.entries(MODEL_GROUPS)) { + if (group.models.some(m => modelId.toLowerCase().includes(m.toLowerCase().split("-")[0]))) { + return groupId; + } + } + // 根据 owned_by 判断 + const model = models.find(m => m.id === modelId); + if (model?.owned_by === "anthropic") return "kiro"; + if (model?.owned_by === "google") return "gemini"; + if (model?.owned_by === "alibaba") return "qwen"; + if (model?.owned_by === "openai") return "openai"; + return null; + }; + + const getGroupBadge = (groupId: string | null) => { + if (!groupId || !MODEL_GROUPS[groupId]) return null; + const group = MODEL_GROUPS[groupId]; + return ( + + {group.name} + + ); + }; + + // 过滤模型 + const filteredModels = models.filter(model => { + const matchesSearch = model.id.toLowerCase().includes(search.toLowerCase()); + const matchesGroup = !selectedGroup || getModelGroup(model.id) === selectedGroup; + return matchesSearch && matchesGroup; + }); + + // 按 provider 分组统计 + const groupCounts = models.reduce((acc, model) => { + const group = getModelGroup(model.id); + if (group) { + acc[group] = (acc[group] || 0) + 1; + } + return acc; + }, {} as Record); + + return ( +
+
+

可用模型

+

查看当前可用的 AI 模型列表

+
+ + {error && ( +
+ {error} +
+ )} + + {/* 搜索和过滤 */} +
+
+ + setSearch(e.target.value)} + className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm" + /> +
+ +
+ + {/* Provider 过滤标签 */} +
+ + {Object.entries(MODEL_GROUPS).map(([groupId, group]) => { + const count = groupCounts[groupId] || 0; + if (count === 0) return null; + return ( + + ); + })} +
+ + {/* 模型列表 */} +
+
+
+ 模型列表 + + {filteredModels.length} 个模型 + +
+
+ + {loading ? ( +
+ +
+ ) : filteredModels.length === 0 ? ( +
+ +

暂无模型数据

+
+ ) : ( +
+ {filteredModels.map((model) => ( +
+
+ +
+
+ {model.id} + {getGroupBadge(getModelGroup(model.id))} +
+

+ {model.owned_by} +

+
+
+ +
+ ))} +
+ )} +
+ + {/* 使用说明 */} +
+

使用说明

+
+

• 点击模型 ID 右侧的复制按钮可快速复制模型名称

+

• 在 API 请求中使用 model 参数指定模型

+

• 不同 Provider 支持的模型不同,请确保已配置对应的凭证

+
+
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8b547b97a..8c9ec75a8 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ -import { LayoutDashboard, Server, Settings, ScrollText } from "lucide-react"; +import { LayoutDashboard, Server, Settings, ScrollText, Cpu } from "lucide-react"; import { cn } from "@/lib/utils"; -type Page = "dashboard" | "providers" | "settings" | "logs"; +type Page = "dashboard" | "providers" | "models" | "settings" | "logs"; interface SidebarProps { currentPage: Page; @@ -11,6 +11,7 @@ interface SidebarProps { const navItems = [ { id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard }, { id: "providers" as Page, label: "Provider", icon: Server }, + { id: "models" as Page, label: "模型", icon: Cpu }, { id: "logs" as Page, label: "日志", icon: ScrollText }, { id: "settings" as Page, label: "设置", icon: Settings }, ]; diff --git a/src/hooks/useTauri.ts b/src/hooks/useTauri.ts index 800d2a35d..86b89ef07 100644 --- a/src/hooks/useTauri.ts +++ b/src/hooks/useTauri.ts @@ -274,3 +274,16 @@ export async function setClaudeCustomConfig( enabled }); } + + +// ============ Models ============ + +export interface ModelInfo { + id: string; + object: string; + owned_by: string; +} + +export async function getAvailableModels(): Promise { + return invoke("get_available_models"); +}