chore: 更新 antigravity-provider 版本到 v0.4.0

This commit is contained in:
coso
2026-01-06 09:10:13 +08:00
parent 4965f6d385
commit 04045c79a1
39 changed files with 4564 additions and 580 deletions
+1 -1
View File
@@ -3674,7 +3674,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.29.0"
version = "0.30.0"
dependencies = [
"anyhow",
"arboard",
+6
View File
@@ -14,6 +14,7 @@ use crate::commands::flow_monitor_cmd::{
SessionManagerState,
};
use crate::commands::machine_id_cmd::MachineIdState;
use crate::commands::model_registry_cmd::ModelRegistryState;
use crate::commands::orchestrator_cmd::OrchestratorState;
use crate::commands::plugin_cmd::PluginManagerState;
use crate::commands::plugin_install_cmd::PluginInstallerState;
@@ -132,6 +133,7 @@ pub struct AppStates {
pub oauth_plugin_manager: crate::commands::oauth_plugin_cmd::OAuthPluginManagerState,
pub orchestrator: OrchestratorState,
pub connect_state: ConnectStateWrapper,
pub model_registry: ModelRegistryState,
// 用于 setup hook 的共享实例
pub shared_stats: Arc<parking_lot::RwLock<telemetry::StatsAggregator>>,
pub shared_tokens: Arc<parking_lot::RwLock<telemetry::TokenTracker>>,
@@ -207,6 +209,9 @@ pub fn init_states(config: &Config) -> Result<AppStates, String> {
// 初始化 Connect 状态(延迟初始化,在 setup hook 中完成)
let connect_state = ConnectStateWrapper(Arc::new(RwLock::new(None)));
// 初始化 Model Registry 状态(延迟初始化,在 setup hook 中完成)
let model_registry_state: ModelRegistryState = Arc::new(RwLock::new(None));
// 初始化默认技能仓库
{
let conn = db.lock().expect("Failed to lock database");
@@ -242,6 +247,7 @@ pub fn init_states(config: &Config) -> Result<AppStates, String> {
oauth_plugin_manager: oauth_plugin_manager_state,
orchestrator: orchestrator_state,
connect_state,
model_registry: model_registry_state,
shared_stats,
shared_tokens,
shared_logger,
+40
View File
@@ -71,6 +71,7 @@ pub fn run() {
oauth_plugin_manager: oauth_plugin_manager_state,
orchestrator: orchestrator_state,
connect_state: connect_state,
model_registry: model_registry_state,
shared_stats,
shared_tokens,
shared_logger,
@@ -145,6 +146,7 @@ pub fn run() {
.manage(oauth_plugin_manager_state)
.manage(orchestrator_state)
.manage(connect_state)
.manage(model_registry_state)
.on_window_event(move |window, event| {
// 处理窗口关闭事件
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
@@ -219,6 +221,33 @@ pub fn run() {
});
}
// 初始化 Model Registry 服务
{
let app_handle = app.handle().clone();
let db_clone = db_clone.clone();
tauri::async_runtime::spawn(async move {
// 创建 ModelRegistryService
let service = crate::services::model_registry_service::ModelRegistryService::new(db_clone);
// 初始化服务
match service.initialize().await {
Ok(()) => {
tracing::info!("[启动] Model Registry 服务初始化成功");
// 更新状态
if let Some(state) = app_handle
.try_state::<crate::commands::model_registry_cmd::ModelRegistryState>()
{
let mut guard = state.write().await;
*guard = Some(service);
}
}
Err(e) => {
tracing::error!("[启动] Model Registry 服务初始化失败: {}", e);
}
}
});
}
// 注册 Deep Link 事件处理器(仅 macOS)
// _Requirements: 1.4_
#[cfg(target_os = "macos")]
@@ -984,6 +1013,17 @@ pub fn run() {
commands::connect_cmd::refresh_relay_registry,
commands::connect_cmd::list_relay_providers,
commands::connect_cmd::send_connect_callback,
// Model Registry commands
commands::model_registry_cmd::get_model_registry,
commands::model_registry_cmd::refresh_model_registry,
commands::model_registry_cmd::search_models,
commands::model_registry_cmd::get_model_preferences,
commands::model_registry_cmd::toggle_model_favorite,
commands::model_registry_cmd::hide_model,
commands::model_registry_cmd::record_model_usage,
commands::model_registry_cmd::get_model_sync_state,
commands::model_registry_cmd::get_models_for_provider,
commands::model_registry_cmd::get_models_by_tier,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+1
View File
@@ -9,6 +9,7 @@ pub mod injection_cmd;
pub mod kiro_local;
pub mod machine_id_cmd;
pub mod mcp_cmd;
pub mod model_registry_cmd;
pub mod models_cmd;
pub mod native_agent_cmd;
pub mod network_cmd;
@@ -0,0 +1,155 @@
//! 模型注册表 Tauri 命令
//!
//! 提供模型注册表相关的前端 API
use crate::models::model_registry::{
EnhancedModelMetadata, ModelSyncState, ModelTier, UserModelPreference,
};
use crate::services::model_registry_service::ModelRegistryService;
use std::sync::Arc;
use tauri::State;
use tokio::sync::RwLock;
/// 模型注册服务状态
pub type ModelRegistryState = Arc<RwLock<Option<ModelRegistryService>>>;
/// 获取所有模型
#[tauri::command]
pub async fn get_model_registry(
state: State<'_, ModelRegistryState>,
) -> Result<Vec<EnhancedModelMetadata>, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
Ok(service.get_all_models().await)
}
/// 刷新模型注册表(从 models.dev 获取最新数据)
#[tauri::command]
pub async fn refresh_model_registry(
state: State<'_, ModelRegistryState>,
) -> Result<(), String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
service.refresh_from_models_dev().await
}
/// 搜索模型
#[tauri::command]
pub async fn search_models(
state: State<'_, ModelRegistryState>,
query: String,
limit: Option<usize>,
) -> Result<Vec<EnhancedModelMetadata>, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
Ok(service.search_models(&query, limit.unwrap_or(50)).await)
}
/// 获取用户模型偏好
#[tauri::command]
pub async fn get_model_preferences(
state: State<'_, ModelRegistryState>,
) -> Result<Vec<UserModelPreference>, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
service.get_all_preferences().await
}
/// 切换模型收藏状态
#[tauri::command]
pub async fn toggle_model_favorite(
state: State<'_, ModelRegistryState>,
model_id: String,
) -> Result<bool, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
service.toggle_favorite(&model_id).await
}
/// 隐藏模型
#[tauri::command]
pub async fn hide_model(
state: State<'_, ModelRegistryState>,
model_id: String,
) -> Result<(), String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
service.hide_model(&model_id).await
}
/// 记录模型使用
#[tauri::command]
pub async fn record_model_usage(
state: State<'_, ModelRegistryState>,
model_id: String,
) -> Result<(), String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
service.record_usage(&model_id).await
}
/// 获取模型同步状态
#[tauri::command]
pub async fn get_model_sync_state(
state: State<'_, ModelRegistryState>,
) -> Result<ModelSyncState, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
Ok(service.get_sync_state().await)
}
/// 按 Provider 获取模型
#[tauri::command]
pub async fn get_models_for_provider(
state: State<'_, ModelRegistryState>,
provider_id: String,
) -> Result<Vec<EnhancedModelMetadata>, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
Ok(service.get_models_by_provider(&provider_id).await)
}
/// 按服务等级获取模型
#[tauri::command]
pub async fn get_models_by_tier(
state: State<'_, ModelRegistryState>,
tier: String,
) -> Result<Vec<EnhancedModelMetadata>, String> {
let guard = state.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
let tier: ModelTier = tier
.parse()
.map_err(|_| format!("无效的服务等级: {}", tier))?;
Ok(service.get_models_by_tier(tier).await)
}
+46 -14
View File
@@ -65,10 +65,13 @@ pub async fn init_orchestrator(
.map(|c| {
// 从 credential 中提取支持的模型列表
let supported_models = extract_supported_models(&c.credential);
// 保存原始的 provider_type 字符串(如 "antigravity"、"kiro" 等)
let original_provider_type = c.provider_type.to_string();
CredentialInfo {
id: c.uuid.clone(),
provider_type: map_pool_provider_type(&c.provider_type.to_string()),
provider_type: map_pool_provider_type(&original_provider_type),
original_provider_type: Some(original_provider_type),
supported_models,
is_healthy: c.is_healthy,
current_load: None,
@@ -95,34 +98,47 @@ fn extract_supported_models(
CredentialData::ClaudeKey { .. } | CredentialData::ClaudeOAuth { .. } => {
vec![
"claude-opus-4-5-20251101".to_string(),
"claude-opus-4-20250514".to_string(),
"claude-sonnet-4-5-20250929".to_string(),
"claude-sonnet-4-20250514".to_string(),
"claude-haiku-4-5-20251001".to_string(),
"claude-3-7-sonnet-20250219".to_string(),
"claude-3-5-haiku-20241022".to_string(),
]
}
CredentialData::OpenAIKey { .. } => {
vec![
"gpt-5.2-codex".to_string(),
"gpt-5.2".to_string(),
"gpt-5.1-codex-max".to_string(),
"gpt-5.1-codex".to_string(),
"gpt-5.1-codex-mini".to_string(),
"gpt-5.1".to_string(),
"gpt-5-codex".to_string(),
"gpt-5-codex-mini".to_string(),
"gpt-5".to_string(),
"gpt-4o".to_string(),
"gpt-4o-mini".to_string(),
"gpt-4-turbo".to_string(),
"o1".to_string(),
"o1-mini".to_string(),
]
}
CredentialData::GeminiOAuth { .. } => {
vec![
"gemini-2.0-flash-exp".to_string(),
"gemini-1.5-pro".to_string(),
"gemini-1.5-flash".to_string(),
"gemini-3-pro-preview".to_string(),
"gemini-3-flash-preview".to_string(),
"gemini-2.5-pro".to_string(),
"gemini-2.5-flash".to_string(),
"gemini-2.5-flash-lite".to_string(),
]
}
CredentialData::GeminiApiKey {
excluded_models, ..
} => {
let all_models = vec![
"gemini-2.0-flash-exp".to_string(),
"gemini-1.5-pro".to_string(),
"gemini-1.5-flash".to_string(),
"gemini-3-pro-preview".to_string(),
"gemini-3-flash-preview".to_string(),
"gemini-2.5-pro".to_string(),
"gemini-2.5-flash".to_string(),
"gemini-2.5-flash-lite".to_string(),
];
all_models
.into_iter()
@@ -131,8 +147,13 @@ fn extract_supported_models(
}
CredentialData::KiroOAuth { .. } => {
vec![
"claude-opus-4-5".to_string(),
"claude-opus-4-5-20251101".to_string(),
"claude-haiku-4-5".to_string(),
"claude-sonnet-4-5".to_string(),
"claude-sonnet-4-5-20250929".to_string(),
"claude-sonnet-4-20250514".to_string(),
"claude-3-7-sonnet-20250219".to_string(),
]
}
CredentialData::CodexOAuth { .. } => {
@@ -140,16 +161,23 @@ fn extract_supported_models(
}
CredentialData::QwenOAuth { .. } => {
vec![
"qwen-max".to_string(),
"qwen-plus".to_string(),
"qwen-turbo".to_string(),
"qwen3-coder-plus".to_string(),
"qwen3-coder-flash".to_string(),
]
}
CredentialData::AntigravityOAuth { .. } => {
vec![
// Max 等级
"gemini-3-pro-preview".to_string(),
"gemini-3-pro-image-preview".to_string(),
"gemini-claude-opus-4-5-thinking".to_string(),
// Pro 等级
"gemini-2.5-flash".to_string(),
"gemini-2.5-computer-use-preview-10-2025".to_string(),
"gemini-claude-sonnet-4-5".to_string(),
"gemini-claude-sonnet-4-5-thinking".to_string(),
"gemini-claude-opus-4-5-thinking".to_string(),
// Mini 等级
"gemini-3-flash-preview".to_string(),
]
}
_ => vec![],
@@ -165,6 +193,7 @@ fn map_pool_provider_type(pool_type: &str) -> ProviderType {
"kiro" => ProviderType::Kiro,
"codex" => ProviderType::OpenAI,
"qwen" => ProviderType::Custom,
"antigravity" => ProviderType::Antigravity,
_ => ProviderType::Custom,
}
}
@@ -233,10 +262,13 @@ pub struct CredentialInfoRequest {
impl From<CredentialInfoRequest> for CredentialInfo {
fn from(req: CredentialInfoRequest) -> Self {
// 保存原始的 provider_type 字符串
let original_provider_type = req.provider_type.clone();
CredentialInfo {
id: req.id,
provider_type: ProviderType::from_str(&req.provider_type)
.unwrap_or(ProviderType::Custom),
original_provider_type: Some(original_provider_type),
supported_models: req.supported_models,
is_healthy: req.is_healthy,
current_load: req.current_load,
@@ -589,13 +589,10 @@ pub fn convert_openai_to_antigravity_with_context(
}
// 转换工具定义
// 注意:Antigravity API 统一使用 Gemini 格式(function_declarations)
// Claude 模型在 Antigravity 内部会自动转换
// 注意:Antigravity API 统一使用 functionDeclarations 格式
// Claude 和 Gemini 模型都使用相同的结构,但字段名可能不同
let tools: Option<serde_json::Value> = request.tools.as_ref().and_then(|tools| {
let is_claude = is_claude_model(actual_model);
// Gemini 模型使用 function_declarations + parametersJsonSchema
// Claude 模型使用 function_declarations + inputSchema(注意字段名不同)
let mut function_declarations: Vec<serde_json::Value> = Vec::new();
for t in tools {
@@ -605,7 +602,8 @@ pub fn convert_openai_to_antigravity_with_context(
.parameters
.as_ref()
.map(|p| {
let mut schema = clean_parameters(Some(p.clone())).unwrap_or_default();
let mut schema =
clean_parameters(Some(p.clone())).unwrap_or_default();
// 确保有 type 和 properties
if schema.get("type").is_none() {
schema["type"] = serde_json::json!("object");
@@ -615,20 +613,22 @@ pub fn convert_openai_to_antigravity_with_context(
}
schema
})
.unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
.unwrap_or_else(|| {
serde_json::json!({"type": "object", "properties": {}})
});
if is_claude {
// Claude 模型使用 inputSchema 字段名
// Claude 模型使用 parameters 字段(标准 Gemini 格式)
function_declarations.push(serde_json::json!({
"name": function.name,
"description": function.description.clone().unwrap_or_default(),
"inputSchema": params_schema
"parameters": params_schema
}));
} else {
// Gemini 模型使用 parametersJsonSchema 字段名
// Gemini 模型使用 parametersJsonSchema 字段
function_declarations.push(serde_json::json!({
"name": function.name,
"description": function.description.clone(),
"description": function.description.clone().unwrap_or_default(),
"parametersJsonSchema": params_schema
}));
}
+48
View File
@@ -539,6 +539,54 @@ impl CredentialProviderRegistry {
info!("Plugin installed to: {:?}", target_dir);
// 尝试下载 UI 资源包(如果存在)
let ui_url = if version_tag == "latest" {
format!(
"https://github.com/{}/{}/releases/latest/download/{}-ui.zip",
owner, repo, repo
)
} else {
format!(
"https://github.com/{}/{}/releases/download/{}/{}-ui.zip",
owner, repo, version_tag, repo
)
};
info!("Trying to download UI assets from: {}", ui_url);
if let Ok(ui_response) = client.get(&ui_url).send().await {
if ui_response.status().is_success() {
if let Ok(ui_bytes) = ui_response.bytes().await {
let ui_cursor = std::io::Cursor::new(ui_bytes);
if let Ok(mut ui_archive) = zip::ZipArchive::new(ui_cursor) {
for i in 0..ui_archive.len() {
if let Ok(mut file) = ui_archive.by_index(i) {
let outpath = target_dir.join(file.name());
if file.name().ends_with('/') {
let _ = std::fs::create_dir_all(&outpath);
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
let _ = std::fs::create_dir_all(p);
}
}
if let Ok(mut outfile) = std::fs::File::create(&outpath) {
let _ = std::io::copy(&mut file, &mut outfile);
}
}
}
}
info!("UI assets installed for plugin: {}", plugin_id);
}
}
} else {
info!("No UI assets available for plugin: {} (HTTP {})", plugin_id, ui_response.status());
}
} else {
info!("No UI assets available for plugin: {}", plugin_id);
}
// 注册插件(创建 PluginInstance)
self.register_from_dir(&target_dir, &plugin_id).await?;
+761
View File
@@ -0,0 +1,761 @@
//! 本地硬编码的国内模型数据
//!
//! 这些模型数据用于补充 models.dev API 未覆盖的国内模型
use crate::models::model_registry::{
EnhancedModelMetadata, ModelCapabilities, ModelLimits, ModelPricing, ModelSource, ModelStatus,
ModelTier,
};
/// 获取所有本地硬编码的国内模型
pub fn get_local_models() -> Vec<EnhancedModelMetadata> {
let mut models = Vec::new();
models.extend(get_dashscope_models());
models.extend(get_zhipu_models());
models.extend(get_baichuan_models());
models.extend(get_moonshot_models());
models.extend(get_deepseek_models());
models.extend(get_doubao_models());
models.extend(get_minimax_models());
models.extend(get_yi_models());
models.extend(get_stepfun_models());
models
}
/// 通义千问系列模型 (阿里云百炼)
fn get_dashscope_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "qwen3-coder-plus".to_string(),
display_name: "通义千问 Coder Plus".to_string(),
provider_id: "dashscope".to_string(),
provider_name: "阿里云百炼".to_string(),
family: Some("qwen-coder".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(4.0), output_per_million: Some(16.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(131072), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2025-01-01".to_string()),
is_latest: true,
description: Some("阿里云通义千问代码模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "qwen-max".to_string(),
display_name: "通义千问 Max".to_string(),
provider_id: "dashscope".to_string(),
provider_name: "阿里云百炼".to_string(),
family: Some("qwen".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: true,
},
pricing: Some(ModelPricing {
input_per_million: Some(20.0), output_per_million: Some(60.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32768), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-12-01".to_string()),
is_latest: true,
description: Some("通义千问旗舰模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "qwen-plus".to_string(),
display_name: "通义千问 Plus".to_string(),
provider_id: "dashscope".to_string(),
provider_name: "阿里云百炼".to_string(),
family: Some("qwen".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(4.0), output_per_million: Some(12.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(131072), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-12-01".to_string()),
is_latest: true,
description: Some("通义千问增强版".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "qwen-turbo".to_string(),
display_name: "通义千问 Turbo".to_string(),
provider_id: "dashscope".to_string(),
provider_name: "阿里云百炼".to_string(),
family: Some("qwen".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(0.3), output_per_million: Some(0.6),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(131072), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-12-01".to_string()),
is_latest: true,
description: Some("通义千问快速版".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 智谱 GLM 系列模型
fn get_zhipu_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "glm-4-plus".to_string(),
display_name: "GLM-4 Plus".to_string(),
provider_id: "zhipu".to_string(),
provider_name: "智谱 AI".to_string(),
family: Some("glm-4".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: true,
},
pricing: Some(ModelPricing {
input_per_million: Some(50.0), output_per_million: Some(50.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(128000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-08-01".to_string()),
is_latest: true,
description: Some("智谱旗舰模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "glm-4-air".to_string(),
display_name: "GLM-4 Air".to_string(),
provider_id: "zhipu".to_string(),
provider_name: "智谱 AI".to_string(),
family: Some("glm-4".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(1.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(128000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("智谱高性价比模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "glm-4-flash".to_string(),
display_name: "GLM-4 Flash".to_string(),
provider_id: "zhipu".to_string(),
provider_name: "智谱 AI".to_string(),
family: Some("glm-4".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(0.1), output_per_million: Some(0.1),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(128000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("智谱快速模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 百川系列模型
fn get_baichuan_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "Baichuan4".to_string(),
display_name: "百川 4".to_string(),
provider_id: "baichuan".to_string(),
provider_name: "百川智能".to_string(),
family: Some("baichuan".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(100.0), output_per_million: Some(100.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32768), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-10-01".to_string()),
is_latest: true,
description: Some("百川旗舰模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "Baichuan3-Turbo".to_string(),
display_name: "百川 3 Turbo".to_string(),
provider_id: "baichuan".to_string(),
provider_name: "百川智能".to_string(),
family: Some("baichuan".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(12.0), output_per_million: Some(12.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32768), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("百川高性价比模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 月之暗面 Moonshot 系列模型
fn get_moonshot_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "moonshot-v1-128k".to_string(),
display_name: "Moonshot V1 128K".to_string(),
provider_id: "moonshot".to_string(),
provider_name: "月之暗面".to_string(),
family: Some("moonshot".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(60.0), output_per_million: Some(60.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(128000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-03-01".to_string()),
is_latest: true,
description: Some("月之暗面长上下文模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "moonshot-v1-32k".to_string(),
display_name: "Moonshot V1 32K".to_string(),
provider_id: "moonshot".to_string(),
provider_name: "月之暗面".to_string(),
family: Some("moonshot".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(24.0), output_per_million: Some(24.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-03-01".to_string()),
is_latest: true,
description: Some("月之暗面标准模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "moonshot-v1-8k".to_string(),
display_name: "Moonshot V1 8K".to_string(),
provider_id: "moonshot".to_string(),
provider_name: "月之暗面".to_string(),
family: Some("moonshot".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(12.0), output_per_million: Some(12.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(8000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-03-01".to_string()),
is_latest: true,
description: Some("月之暗面快速模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// DeepSeek 系列模型
fn get_deepseek_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "deepseek-chat".to_string(),
display_name: "DeepSeek Chat".to_string(),
provider_id: "deepseek".to_string(),
provider_name: "DeepSeek".to_string(),
family: Some("deepseek".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(2.0),
cache_read_per_million: Some(0.1), cache_write_per_million: Some(1.0),
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(64000), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-12-01".to_string()),
is_latest: true,
description: Some("DeepSeek V3 对话模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "deepseek-reasoner".to_string(),
display_name: "DeepSeek Reasoner".to_string(),
provider_id: "deepseek".to_string(),
provider_name: "DeepSeek".to_string(),
family: Some("deepseek".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: false, tools: false, streaming: true,
json_mode: false, function_calling: false, reasoning: true,
},
pricing: Some(ModelPricing {
input_per_million: Some(4.0), output_per_million: Some(16.0),
cache_read_per_million: Some(0.4), cache_write_per_million: Some(4.0),
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(64000), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2025-01-01".to_string()),
is_latest: true,
description: Some("DeepSeek R1 推理模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "deepseek-coder".to_string(),
display_name: "DeepSeek Coder".to_string(),
provider_id: "deepseek".to_string(),
provider_name: "DeepSeek".to_string(),
family: Some("deepseek-coder".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(2.0),
cache_read_per_million: Some(0.1), cache_write_per_million: Some(1.0),
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(64000), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("DeepSeek 代码模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 字节豆包系列模型
fn get_doubao_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "doubao-pro-256k".to_string(),
display_name: "豆包 Pro 256K".to_string(),
provider_id: "doubao".to_string(),
provider_name: "字节跳动".to_string(),
family: Some("doubao".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(5.0), output_per_million: Some(9.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(256000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-10-01".to_string()),
is_latest: true,
description: Some("豆包旗舰长上下文模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "doubao-pro-32k".to_string(),
display_name: "豆包 Pro 32K".to_string(),
provider_id: "doubao".to_string(),
provider_name: "字节跳动".to_string(),
family: Some("doubao".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(0.8), output_per_million: Some(2.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("豆包标准模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "doubao-lite-32k".to_string(),
display_name: "豆包 Lite 32K".to_string(),
provider_id: "doubao".to_string(),
provider_name: "字节跳动".to_string(),
family: Some("doubao".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(0.3), output_per_million: Some(0.6),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("豆包轻量模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// MiniMax 系列模型
fn get_minimax_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "abab6.5s-chat".to_string(),
display_name: "MiniMax abab6.5s".to_string(),
provider_id: "minimax".to_string(),
provider_name: "MiniMax".to_string(),
family: Some("abab".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(1.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(245760), max_output_tokens: Some(8192),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("MiniMax 长上下文模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 零一万物 Yi 系列模型
fn get_yi_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "yi-large".to_string(),
display_name: "Yi Large".to_string(),
provider_id: "yi".to_string(),
provider_name: "零一万物".to_string(),
family: Some("yi".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(20.0), output_per_million: Some(20.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(32768), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-05-01".to_string()),
is_latest: true,
description: Some("零一万物旗舰模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "yi-medium".to_string(),
display_name: "Yi Medium".to_string(),
provider_id: "yi".to_string(),
provider_name: "零一万物".to_string(),
family: Some("yi".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(2.5), output_per_million: Some(2.5),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(16384), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-05-01".to_string()),
is_latest: true,
description: Some("零一万物标准模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "yi-spark".to_string(),
display_name: "Yi Spark".to_string(),
provider_id: "yi".to_string(),
provider_name: "零一万物".to_string(),
family: Some("yi".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(1.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(16384), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-05-01".to_string()),
is_latest: true,
description: Some("零一万物快速模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
/// 阶跃星辰 Step 系列模型
fn get_stepfun_models() -> Vec<EnhancedModelMetadata> {
let now = chrono::Utc::now().timestamp();
vec![
EnhancedModelMetadata {
id: "step-2-16k".to_string(),
display_name: "Step 2 16K".to_string(),
provider_id: "stepfun".to_string(),
provider_name: "阶跃星辰".to_string(),
family: Some("step".to_string()),
tier: ModelTier::Max,
capabilities: ModelCapabilities {
vision: true, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(38.0), output_per_million: Some(120.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(16384), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-09-01".to_string()),
is_latest: true,
description: Some("阶跃星辰旗舰模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "step-1-128k".to_string(),
display_name: "Step 1 128K".to_string(),
provider_id: "stepfun".to_string(),
provider_name: "阶跃星辰".to_string(),
family: Some("step".to_string()),
tier: ModelTier::Pro,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(40.0), output_per_million: Some(100.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(128000), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("阶跃星辰长上下文模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
EnhancedModelMetadata {
id: "step-1-flash".to_string(),
display_name: "Step 1 Flash".to_string(),
provider_id: "stepfun".to_string(),
provider_name: "阶跃星辰".to_string(),
family: Some("step".to_string()),
tier: ModelTier::Mini,
capabilities: ModelCapabilities {
vision: false, tools: true, streaming: true,
json_mode: true, function_calling: true, reasoning: false,
},
pricing: Some(ModelPricing {
input_per_million: Some(1.0), output_per_million: Some(4.0),
cache_read_per_million: None, cache_write_per_million: None,
currency: "CNY".to_string(),
}),
limits: ModelLimits {
context_length: Some(8192), max_output_tokens: Some(4096),
requests_per_minute: None, tokens_per_minute: None,
},
status: ModelStatus::Active,
release_date: Some("2024-06-01".to_string()),
is_latest: true,
description: Some("阶跃星辰快速模型".to_string()),
source: ModelSource::Local,
created_at: now, updated_at: now,
},
]
}
+7
View File
@@ -0,0 +1,7 @@
//! 静态数据模块
//!
//! 包含本地硬编码的模型数据等
pub mod local_models;
pub use local_models::get_local_models;
+83
View File
@@ -454,6 +454,89 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> {
// ProxyCast Connect 相关表
// ============================================================================
// ============================================================================
// Model Registry 相关表 (借鉴 opencode 的模型管理方式)
// ============================================================================
// 增强的模型注册表
// 存储从 models.dev API 获取的模型数据 + 本地补充的国内模型数据
conn.execute(
"CREATE TABLE IF NOT EXISTS model_registry (
id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
provider_id TEXT NOT NULL,
provider_name TEXT NOT NULL,
family TEXT,
tier TEXT NOT NULL DEFAULT 'pro',
capabilities TEXT NOT NULL DEFAULT '{}',
pricing TEXT,
limits TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
release_date TEXT,
is_latest INTEGER DEFAULT 0,
description TEXT,
source TEXT NOT NULL DEFAULT 'local',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
// 创建 model_registry 索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_registry_provider ON model_registry(provider_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_registry_tier ON model_registry(tier)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_registry_family ON model_registry(family)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_registry_source ON model_registry(source)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_registry_status ON model_registry(status)",
[],
)?;
// 用户模型偏好表
// 存储用户的收藏、隐藏、使用统计等偏好
conn.execute(
"CREATE TABLE IF NOT EXISTS user_model_preferences (
model_id TEXT PRIMARY KEY,
is_favorite INTEGER DEFAULT 0,
is_hidden INTEGER DEFAULT 0,
custom_alias TEXT,
usage_count INTEGER DEFAULT 0,
last_used_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
// 创建 user_model_preferences 索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_user_model_preferences_favorite ON user_model_preferences(is_favorite)",
[],
)?;
// 模型同步状态表
// 记录 models.dev API 同步状态
conn.execute(
"CREATE TABLE IF NOT EXISTS model_sync_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
)",
[],
)?;
Ok(())
}
+1
View File
@@ -31,6 +31,7 @@ pub mod websocket;
mod commands;
mod config;
mod converter;
mod data;
mod logger;
mod models;
mod providers;
+1
View File
@@ -4,6 +4,7 @@ pub mod codewhisperer;
pub mod kiro_fingerprint;
pub mod machine_id;
pub mod mcp_model;
pub mod model_registry;
pub mod openai;
pub mod prompt_model;
pub mod provider_model;
+608
View File
@@ -0,0 +1,608 @@
//! 模型注册表数据结构
//!
//! 借鉴 opencode 的模型管理方式,定义增强的模型元数据结构
use serde::{Deserialize, Serialize};
/// 模型能力
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ModelCapabilities {
/// 是否支持视觉输入
pub vision: bool,
/// 是否支持工具调用
pub tools: bool,
/// 是否支持流式输出
pub streaming: bool,
/// 是否支持 JSON 模式
pub json_mode: bool,
/// 是否支持函数调用
pub function_calling: bool,
/// 是否支持推理/思考
pub reasoning: bool,
}
/// 模型定价
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPricing {
/// 输入价格(每百万 token)
pub input_per_million: Option<f64>,
/// 输出价格(每百万 token)
pub output_per_million: Option<f64>,
/// 缓存读取价格(每百万 token)
pub cache_read_per_million: Option<f64>,
/// 缓存写入价格(每百万 token)
pub cache_write_per_million: Option<f64>,
/// 货币单位 ("USD" | "CNY")
pub currency: String,
}
impl Default for ModelPricing {
fn default() -> Self {
Self {
input_per_million: None,
output_per_million: None,
cache_read_per_million: None,
cache_write_per_million: None,
currency: "USD".to_string(),
}
}
}
/// 模型限制
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ModelLimits {
/// 上下文长度
pub context_length: Option<u32>,
/// 最大输出 token 数
pub max_output_tokens: Option<u32>,
/// 每分钟请求数限制
pub requests_per_minute: Option<u32>,
/// 每分钟 token 数限制
pub tokens_per_minute: Option<u32>,
}
/// 模型状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModelStatus {
/// 活跃可用
Active,
/// 预览版
Preview,
/// Alpha 测试
Alpha,
/// Beta 测试
Beta,
/// 已弃用
Deprecated,
/// 旧版本
Legacy,
}
impl Default for ModelStatus {
fn default() -> Self {
Self::Active
}
}
impl std::fmt::Display for ModelStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Active => write!(f, "active"),
Self::Preview => write!(f, "preview"),
Self::Alpha => write!(f, "alpha"),
Self::Beta => write!(f, "beta"),
Self::Deprecated => write!(f, "deprecated"),
Self::Legacy => write!(f, "legacy"),
}
}
}
impl std::str::FromStr for ModelStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"active" => Ok(Self::Active),
"preview" => Ok(Self::Preview),
"alpha" => Ok(Self::Alpha),
"beta" => Ok(Self::Beta),
"deprecated" => Ok(Self::Deprecated),
"legacy" => Ok(Self::Legacy),
_ => Err(format!("Unknown model status: {}", s)),
}
}
}
/// 模型服务等级
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModelTier {
/// 快速响应,适合简单任务
Mini,
/// 均衡性能,适合大多数任务
Pro,
/// 最强能力,适合复杂任务
Max,
}
impl Default for ModelTier {
fn default() -> Self {
Self::Pro
}
}
impl std::fmt::Display for ModelTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Mini => write!(f, "mini"),
Self::Pro => write!(f, "pro"),
Self::Max => write!(f, "max"),
}
}
}
impl std::str::FromStr for ModelTier {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"mini" => Ok(Self::Mini),
"pro" => Ok(Self::Pro),
"max" => Ok(Self::Max),
_ => Err(format!("Unknown model tier: {}", s)),
}
}
}
/// 模型数据来源
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModelSource {
/// 从 models.dev API 获取
ModelsDev,
/// 本地硬编码(国内模型等)
Local,
/// 用户自定义
Custom,
}
impl Default for ModelSource {
fn default() -> Self {
Self::Local
}
}
impl std::fmt::Display for ModelSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ModelsDev => write!(f, "models.dev"),
Self::Local => write!(f, "local"),
Self::Custom => write!(f, "custom"),
}
}
}
impl std::str::FromStr for ModelSource {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"models.dev" | "modelsdev" => Ok(Self::ModelsDev),
"local" => Ok(Self::Local),
"custom" => Ok(Self::Custom),
_ => Err(format!("Unknown model source: {}", s)),
}
}
}
/// 增强的模型元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnhancedModelMetadata {
/// 模型 ID (如 "claude-sonnet-4-5-20250514")
pub id: String,
/// 显示名称 (如 "Claude Sonnet 4.5")
pub display_name: String,
/// Provider ID (如 "anthropic", "openai", "dashscope")
pub provider_id: String,
/// Provider 显示名称
pub provider_name: String,
/// 模型家族 (如 "sonnet", "gpt-4", "qwen")
pub family: Option<String>,
/// 服务等级
pub tier: ModelTier,
/// 模型能力
pub capabilities: ModelCapabilities,
/// 定价信息
pub pricing: Option<ModelPricing>,
/// 限制信息
pub limits: ModelLimits,
/// 模型状态
pub status: ModelStatus,
/// 发布日期
pub release_date: Option<String>,
/// 是否为最新版本
pub is_latest: bool,
/// 描述
pub description: Option<String>,
/// 数据来源
pub source: ModelSource,
/// 创建时间 (Unix 时间戳)
pub created_at: i64,
/// 最后更新时间 (Unix 时间戳)
pub updated_at: i64,
}
impl EnhancedModelMetadata {
/// 创建新的模型元数据
pub fn new(
id: String,
display_name: String,
provider_id: String,
provider_name: String,
) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
id,
display_name,
provider_id,
provider_name,
family: None,
tier: ModelTier::Pro,
capabilities: ModelCapabilities::default(),
pricing: None,
limits: ModelLimits::default(),
status: ModelStatus::Active,
release_date: None,
is_latest: false,
description: None,
source: ModelSource::Local,
created_at: now,
updated_at: now,
}
}
/// 设置模型家族
pub fn with_family(mut self, family: impl Into<String>) -> Self {
self.family = Some(family.into());
self
}
/// 设置服务等级
pub fn with_tier(mut self, tier: ModelTier) -> Self {
self.tier = tier;
self
}
/// 设置模型能力
pub fn with_capabilities(mut self, capabilities: ModelCapabilities) -> Self {
self.capabilities = capabilities;
self
}
/// 设置定价信息
pub fn with_pricing(mut self, pricing: ModelPricing) -> Self {
self.pricing = Some(pricing);
self
}
/// 设置限制信息
pub fn with_limits(mut self, limits: ModelLimits) -> Self {
self.limits = limits;
self
}
/// 设置模型状态
pub fn with_status(mut self, status: ModelStatus) -> Self {
self.status = status;
self
}
/// 设置发布日期
pub fn with_release_date(mut self, date: impl Into<String>) -> Self {
self.release_date = Some(date.into());
self
}
/// 设置是否为最新版本
pub fn with_is_latest(mut self, is_latest: bool) -> Self {
self.is_latest = is_latest;
self
}
/// 设置描述
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// 设置数据来源
pub fn with_source(mut self, source: ModelSource) -> Self {
self.source = source;
self
}
}
/// 用户模型偏好
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserModelPreference {
/// 模型 ID
pub model_id: String,
/// 是否收藏
pub is_favorite: bool,
/// 是否隐藏
pub is_hidden: bool,
/// 自定义别名
pub custom_alias: Option<String>,
/// 使用次数
pub usage_count: u32,
/// 最后使用时间 (Unix 时间戳)
pub last_used_at: Option<i64>,
/// 创建时间 (Unix 时间戳)
pub created_at: i64,
/// 更新时间 (Unix 时间戳)
pub updated_at: i64,
}
impl UserModelPreference {
/// 创建新的用户偏好
pub fn new(model_id: String) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
model_id,
is_favorite: false,
is_hidden: false,
custom_alias: None,
usage_count: 0,
last_used_at: None,
created_at: now,
updated_at: now,
}
}
}
/// 模型同步状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelSyncState {
/// 最后同步时间 (Unix 时间戳)
pub last_sync_at: Option<i64>,
/// 同步的模型数量
pub model_count: u32,
/// 是否正在同步
pub is_syncing: bool,
/// 最后同步错误
pub last_error: Option<String>,
}
impl Default for ModelSyncState {
fn default() -> Self {
Self {
last_sync_at: None,
model_count: 0,
is_syncing: false,
last_error: None,
}
}
}
/// models.dev API 响应中的 Provider 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevProvider {
pub id: String,
pub name: String,
#[serde(default)]
pub api: Option<String>,
#[serde(default)]
pub npm: Option<String>,
#[serde(default)]
pub models: std::collections::HashMap<String, ModelsDevModel>,
}
/// models.dev API 响应中的 Model 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevModel {
pub id: String,
pub name: String,
#[serde(default)]
pub family: Option<String>,
#[serde(default)]
pub release_date: Option<String>,
#[serde(default)]
pub attachment: bool,
#[serde(default)]
pub reasoning: bool,
#[serde(default)]
pub temperature: bool,
#[serde(default)]
pub tool_call: bool,
#[serde(default)]
pub cost: Option<ModelsDevCost>,
#[serde(default)]
pub limit: Option<ModelsDevLimit>,
#[serde(default)]
pub modalities: Option<ModelsDevModalities>,
#[serde(default)]
pub experimental: Option<bool>,
#[serde(default)]
pub status: Option<String>,
}
/// models.dev API 响应中的 Cost 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevCost {
#[serde(default)]
pub input: Option<f64>,
#[serde(default)]
pub output: Option<f64>,
#[serde(default)]
pub cache_read: Option<f64>,
#[serde(default)]
pub cache_write: Option<f64>,
}
/// models.dev API 响应中的 Limit 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevLimit {
#[serde(default)]
pub context: Option<u32>,
#[serde(default)]
pub output: Option<u32>,
}
/// models.dev API 响应中的 Modalities 结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevModalities {
#[serde(default)]
pub input: Vec<String>,
#[serde(default)]
pub output: Vec<String>,
}
impl ModelsDevModel {
/// 转换为 EnhancedModelMetadata
pub fn to_enhanced_metadata(
&self,
provider_id: &str,
provider_name: &str,
) -> EnhancedModelMetadata {
let now = chrono::Utc::now().timestamp();
// 判断是否支持视觉
let supports_vision = self
.modalities
.as_ref()
.map(|m| m.input.iter().any(|i| i == "image" || i == "video"))
.unwrap_or(false)
|| self.attachment;
// 根据模型名称推断服务等级
let tier = infer_model_tier(&self.id, &self.name);
// 解析状态
let status = self
.status
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(ModelStatus::Active);
// 判断是否为最新版本
let is_latest = self.id.contains("latest");
EnhancedModelMetadata {
id: self.id.clone(),
display_name: self.name.clone(),
provider_id: provider_id.to_string(),
provider_name: provider_name.to_string(),
family: self.family.clone(),
tier,
capabilities: ModelCapabilities {
vision: supports_vision,
tools: self.tool_call,
streaming: true, // 大多数模型都支持流式
json_mode: true, // 大多数模型都支持 JSON 模式
function_calling: self.tool_call,
reasoning: self.reasoning,
},
pricing: self.cost.as_ref().map(|c| ModelPricing {
input_per_million: c.input,
output_per_million: c.output,
cache_read_per_million: c.cache_read,
cache_write_per_million: c.cache_write,
currency: "USD".to_string(),
}),
limits: ModelLimits {
context_length: self.limit.as_ref().and_then(|l| l.context),
max_output_tokens: self.limit.as_ref().and_then(|l| l.output),
requests_per_minute: None,
tokens_per_minute: None,
},
status,
release_date: self.release_date.clone(),
is_latest,
description: None,
source: ModelSource::ModelsDev,
created_at: now,
updated_at: now,
}
}
}
/// 根据模型 ID 和名称推断服务等级
fn infer_model_tier(model_id: &str, model_name: &str) -> ModelTier {
let id_lower = model_id.to_lowercase();
let name_lower = model_name.to_lowercase();
// Max 等级模型
let max_patterns = [
"opus",
"gpt-4o",
"gpt-4-turbo",
"gemini-2.5-pro",
"gemini-ultra",
"claude-3-opus",
"qwen-max",
"glm-4-plus",
"deepseek-v3",
];
for pattern in max_patterns {
if id_lower.contains(pattern) || name_lower.contains(pattern) {
return ModelTier::Max;
}
}
// Mini 等级模型
let mini_patterns = [
"mini",
"nano",
"lite",
"flash",
"haiku",
"gpt-4o-mini",
"gemini-flash",
"qwen-turbo",
"glm-4-flash",
];
for pattern in mini_patterns {
if id_lower.contains(pattern) || name_lower.contains(pattern) {
return ModelTier::Mini;
}
}
// 默认为 Pro 等级
ModelTier::Pro
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_tier_inference() {
assert_eq!(
infer_model_tier("claude-opus-4-5-20250514", "Claude Opus 4.5"),
ModelTier::Max
);
assert_eq!(
infer_model_tier("gpt-4o-mini", "GPT-4o Mini"),
ModelTier::Mini
);
assert_eq!(
infer_model_tier("claude-sonnet-4-5", "Claude Sonnet 4.5"),
ModelTier::Pro
);
assert_eq!(
infer_model_tier("gemini-2.5-flash", "Gemini 2.5 Flash"),
ModelTier::Mini
);
}
#[test]
fn test_model_status_parsing() {
assert_eq!("active".parse::<ModelStatus>().unwrap(), ModelStatus::Active);
assert_eq!(
"deprecated".parse::<ModelStatus>().unwrap(),
ModelStatus::Deprecated
);
assert_eq!("beta".parse::<ModelStatus>().unwrap(), ModelStatus::Beta);
}
}
@@ -324,6 +324,7 @@ mod tests {
.update_credentials(vec![CredentialInfo {
id: "cred-1".to_string(),
provider_type: ProviderType::Anthropic,
original_provider_type: None,
supported_models: vec![
"claude-sonnet-4-5-20250514".to_string(),
"claude-3-5-haiku-20241022".to_string(),
@@ -351,6 +352,7 @@ mod tests {
.update_credentials(vec![CredentialInfo {
id: "cred-1".to_string(),
provider_type: ProviderType::Anthropic,
original_provider_type: None,
supported_models: vec![
"claude-sonnet-4-5-20250514".to_string(),
"claude-3-5-haiku-20241022".to_string(),
+76 -4
View File
@@ -16,6 +16,7 @@ pub enum ProviderType {
Kiro,
Azure,
Bedrock,
Antigravity,
Custom,
}
@@ -29,6 +30,7 @@ impl ProviderType {
"kiro" | "codewhisperer" => Some(ProviderType::Kiro),
"azure" => Some(ProviderType::Azure),
"bedrock" => Some(ProviderType::Bedrock),
"antigravity" => Some(ProviderType::Antigravity),
_ => Some(ProviderType::Custom),
}
}
@@ -42,6 +44,7 @@ impl ProviderType {
ProviderType::Kiro => "Kiro",
ProviderType::Azure => "Azure",
ProviderType::Bedrock => "Bedrock",
ProviderType::Antigravity => "Antigravity",
ProviderType::Custom => "Custom",
}
}
@@ -83,13 +86,30 @@ impl ProviderDefinition {
// 简单的 glob 匹配
let parts: Vec<&str> = pattern_lower.split('*').collect();
if parts.len() == 2 {
// 模式如 "claude-*" 或 "*sonnet"
let prefix = parts[0];
let suffix = parts[1];
model_lower.starts_with(prefix) && model_lower.ends_with(suffix)
if prefix.is_empty() {
model_lower.ends_with(suffix)
} else if suffix.is_empty() {
model_lower.starts_with(prefix)
} else {
model_lower.starts_with(prefix) && model_lower.ends_with(suffix)
}
} else if parts.len() == 3 {
// 模式如 "claude-*opus*" (prefix*middle*suffix)
let prefix = parts[0];
let middle = parts[1];
let suffix = parts[2];
let starts_ok = prefix.is_empty() || model_lower.starts_with(prefix);
let ends_ok = suffix.is_empty() || model_lower.ends_with(suffix);
let contains_middle = middle.is_empty() || model_lower.contains(middle);
starts_ok && ends_ok && contains_middle
} else if parts.len() == 1 {
model_lower.starts_with(parts[0])
} else {
false
// 复杂模式,回退到简单包含检查
parts.iter().all(|p| p.is_empty() || model_lower.contains(p))
}
} else {
model_lower.contains(&pattern_lower)
@@ -221,6 +241,47 @@ pub fn builtin_provider_definitions() -> Vec<ProviderDefinition> {
],
default_base_url: None,
},
// Antigravity (Google Cloud Code Assist)
ProviderDefinition {
provider_type: ProviderType::Antigravity,
display_name: "Antigravity".to_string(),
families: vec![
// Max 等级:Gemini 3 Pro 和 Claude Opus
ModelFamily {
name: "gemini-3-pro".to_string(),
pattern: "gemini-3-pro*".to_string(),
tier: 3,
description: Some("Gemini 3 Pro via Antigravity".to_string()),
},
ModelFamily {
name: "opus".to_string(),
pattern: "*opus*".to_string(),
tier: 3,
description: Some("Claude Opus via Antigravity".to_string()),
},
// Pro 等级:Claude Sonnet 和 Gemini 2.5
ModelFamily {
name: "sonnet".to_string(),
pattern: "*sonnet*".to_string(),
tier: 2,
description: Some("Claude Sonnet via Antigravity".to_string()),
},
ModelFamily {
name: "gemini-2.5".to_string(),
pattern: "gemini-2.5*".to_string(),
tier: 2,
description: Some("Gemini 2.5 via Antigravity".to_string()),
},
// Mini 等级:Flash 模型
ModelFamily {
name: "gemini-3-flash".to_string(),
pattern: "gemini-3-flash*".to_string(),
tier: 1,
description: Some("Gemini 3 Flash via Antigravity".to_string()),
},
],
default_base_url: None,
},
]
}
@@ -395,8 +456,10 @@ pub fn builtin_model_metadata() -> Vec<ModelMetadata> {
pub struct CredentialInfo {
/// 凭证 ID
pub id: String,
/// Provider 类型
/// Provider 类型(用于模型分类)
pub provider_type: ProviderType,
/// 原始 Provider 类型字符串(用于前端识别,如 "antigravity"、"kiro" 等)
pub original_provider_type: Option<String>,
/// 支持的模型列表
pub supported_models: Vec<String>,
/// 是否健康
@@ -476,13 +539,19 @@ impl DynamicPoolBuilder {
.or_else(|| metadata.as_ref().and_then(|m| m.family.clone()));
// 构建 AvailableModel
// 优先使用原始 provider 类型(如 "antigravity"),否则使用枚举名称
let provider_type_str = credential
.original_provider_type
.clone()
.unwrap_or_else(|| format!("{:?}", credential.provider_type).to_lowercase());
let available_model = AvailableModel {
id: model_id.clone(),
display_name: metadata
.as_ref()
.map(|m| m.display_name.clone())
.unwrap_or_else(|| model_id.clone()),
provider_type: format!("{:?}", credential.provider_type).to_lowercase(),
provider_type: provider_type_str,
family,
credential_id: credential.id.clone(),
context_length: metadata.as_ref().and_then(|m| m.context_length),
@@ -590,6 +659,7 @@ mod tests {
CredentialInfo {
id: "cred-1".to_string(),
provider_type: ProviderType::Anthropic,
original_provider_type: None,
supported_models: vec![
"claude-opus-4-5-20251101".to_string(),
"claude-sonnet-4-5-20250514".to_string(),
@@ -601,6 +671,7 @@ mod tests {
CredentialInfo {
id: "cred-2".to_string(),
provider_type: ProviderType::OpenAI,
original_provider_type: None,
supported_models: vec!["gpt-4o".to_string(), "gpt-3.5-turbo".to_string()],
is_healthy: true,
current_load: Some(20),
@@ -622,6 +693,7 @@ mod tests {
let credentials = vec![CredentialInfo {
id: "cred-1".to_string(),
provider_type: ProviderType::Anthropic,
original_provider_type: None,
supported_models: vec![
"claude-sonnet-4-5-20250514".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
@@ -2946,6 +2946,7 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S
}
let mut text = String::new();
let mut thinking_text = String::new();
let mut images = Vec::new();
for candidate in candidates {
@@ -2955,8 +2956,35 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S
.and_then(|p| p.as_array())
{
for part in parts {
// 检查是否是思维内容
let is_thought = part
.get("thought")
.and_then(|t| t.as_bool())
.unwrap_or(false);
// 跳过纯 thoughtSignature 部分
let has_thought_signature = part
.get("thoughtSignature")
.or_else(|| part.get("thought_signature"))
.and_then(|s| s.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
let has_content = part.get("text").is_some()
|| part.get("inlineData").is_some()
|| part.get("inline_data").is_some();
if has_thought_signature && !has_content {
continue;
}
if let Some(t) = part.get("text").and_then(|t| t.as_str()) {
text.push_str(t);
if is_thought {
// 思维内容
thinking_text.push_str(t);
} else {
text.push_str(t);
}
}
if let Some(inline_data) =
part.get("inlineData").or_else(|| part.get("inline_data"))
@@ -2974,10 +3002,19 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S
}
}
if text.is_empty() && images.is_empty() {
// 如果有 thinking 内容,用 <thinking> 标签包裹并放在前面
let mut final_text = String::new();
if !thinking_text.is_empty() {
final_text.push_str("<thinking>");
final_text.push_str(&thinking_text);
final_text.push_str("</thinking>\n\n");
}
final_text.push_str(&text);
if final_text.is_empty() && images.is_empty() {
None
} else {
Some((text, images))
Some((final_text, images))
}
}
+1
View File
@@ -5,6 +5,7 @@ pub mod live_sync;
pub mod machine_id_service;
pub mod mcp_service;
pub mod mcp_sync;
pub mod model_registry_service;
pub mod prompt_service;
pub mod prompt_sync;
pub mod provider_pool_service;
@@ -0,0 +1,657 @@
//! 模型注册服务
//!
//! 负责从 models.dev API 获取模型数据、管理本地缓存、提供模型搜索等功能
use crate::data::get_local_models;
use crate::database::DbConnection;
use crate::models::model_registry::{
EnhancedModelMetadata, ModelSource, ModelStatus,
ModelSyncState, ModelTier, ModelsDevProvider, UserModelPreference,
};
use rusqlite::params;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
const CACHE_DURATION_SECS: i64 = 3600; // 1 小时
/// 模型注册服务
pub struct ModelRegistryService {
/// 数据库连接
db: DbConnection,
/// 内存缓存的模型数据
models_cache: Arc<RwLock<Vec<EnhancedModelMetadata>>>,
/// 同步状态
sync_state: Arc<RwLock<ModelSyncState>>,
}
impl ModelRegistryService {
/// 创建新的模型注册服务
pub fn new(db: DbConnection) -> Self {
Self {
db,
models_cache: Arc::new(RwLock::new(Vec::new())),
sync_state: Arc::new(RwLock::new(ModelSyncState::default())),
}
}
/// 初始化服务
pub async fn initialize(&self) -> Result<(), String> {
tracing::info!("[ModelRegistry] 初始化模型注册服务");
// 1. 尝试从数据库加载缓存
match self.load_from_db().await {
Ok(models) if !models.is_empty() => {
tracing::info!("[ModelRegistry] 从数据库加载了 {} 个模型", models.len());
let mut cache = self.models_cache.write().await;
*cache = models;
// 检查是否需要后台刷新
if self.should_refresh().await {
tracing::info!("[ModelRegistry] 缓存已过期,启动后台刷新");
self.spawn_background_refresh();
}
return Ok(());
}
Ok(_) => {
tracing::info!("[ModelRegistry] 数据库中没有缓存数据");
}
Err(e) => {
tracing::warn!("[ModelRegistry] 从数据库加载失败: {}", e);
}
}
// 2. 使用本地硬编码数据作为初始数据
let local_models = get_local_models();
tracing::info!(
"[ModelRegistry] 使用 {} 个本地硬编码模型作为初始数据",
local_models.len()
);
{
let mut cache = self.models_cache.write().await;
*cache = local_models.clone();
}
// 保存到数据库
if let Err(e) = self.save_models_to_db(&local_models).await {
tracing::warn!("[ModelRegistry] 保存本地模型到数据库失败: {}", e);
}
// 3. 后台获取 models.dev 数据
self.spawn_background_refresh();
Ok(())
}
/// 检查是否需要刷新
async fn should_refresh(&self) -> bool {
let state = self.sync_state.read().await;
match state.last_sync_at {
Some(last_sync) => {
let now = chrono::Utc::now().timestamp();
now - last_sync > CACHE_DURATION_SECS
}
None => true,
}
}
/// 启动后台刷新任务
fn spawn_background_refresh(&self) {
let db = self.db.clone();
let models_cache = self.models_cache.clone();
let sync_state = self.sync_state.clone();
tokio::spawn(async move {
let service = ModelRegistryService {
db,
models_cache,
sync_state,
};
if let Err(e) = service.refresh_from_models_dev().await {
tracing::error!("[ModelRegistry] 后台刷新失败: {}", e);
}
});
}
/// 从 models.dev API 刷新数据
pub async fn refresh_from_models_dev(&self) -> Result<(), String> {
tracing::info!("[ModelRegistry] 开始从 models.dev 获取数据");
// 设置同步状态
{
let mut state = self.sync_state.write().await;
state.is_syncing = true;
state.last_error = None;
}
// 获取数据
let result = self.fetch_models_dev_data().await;
match result {
Ok(models_dev_models) => {
// 合并本地模型
let local_models = get_local_models();
let merged = self.merge_models(models_dev_models, local_models);
tracing::info!(
"[ModelRegistry] 获取并合并了 {} 个模型",
merged.len()
);
// 更新缓存
{
let mut cache = self.models_cache.write().await;
*cache = merged.clone();
}
// 保存到数据库
self.save_models_to_db(&merged).await?;
// 更新同步状态
{
let mut state = self.sync_state.write().await;
state.is_syncing = false;
state.last_sync_at = Some(chrono::Utc::now().timestamp());
state.model_count = merged.len() as u32;
state.last_error = None;
}
// 保存同步状态到数据库
self.save_sync_state().await?;
Ok(())
}
Err(e) => {
tracing::error!("[ModelRegistry] 从 models.dev 获取数据失败: {}", e);
// 更新同步状态
{
let mut state = self.sync_state.write().await;
state.is_syncing = false;
state.last_error = Some(e.clone());
}
Err(e)
}
}
}
/// 从 models.dev API 获取数据
async fn fetch_models_dev_data(&self) -> Result<Vec<EnhancedModelMetadata>, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
let response = client
.get(MODELS_DEV_API_URL)
.header("User-Agent", "ProxyCast/1.0")
.send()
.await
.map_err(|e| format!("请求 models.dev 失败: {}", e))?;
if !response.status().is_success() {
return Err(format!(
"models.dev 返回错误状态码: {}",
response.status()
));
}
let data: HashMap<String, ModelsDevProvider> = response
.json()
.await
.map_err(|e| format!("解析 models.dev 响应失败: {}", e))?;
// 转换为内部格式
let mut models = Vec::new();
for (provider_id, provider) in data {
for (_, model) in provider.models {
let enhanced = model.to_enhanced_metadata(&provider_id, &provider.name);
models.push(enhanced);
}
}
tracing::info!(
"[ModelRegistry] 从 models.dev 获取了 {} 个模型",
models.len()
);
Ok(models)
}
/// 合并 models.dev 数据和本地数据
fn merge_models(
&self,
models_dev: Vec<EnhancedModelMetadata>,
local: Vec<EnhancedModelMetadata>,
) -> Vec<EnhancedModelMetadata> {
let mut merged: HashMap<String, EnhancedModelMetadata> = HashMap::new();
// 先添加 models.dev 数据
for model in models_dev {
merged.insert(model.id.clone(), model);
}
// 本地数据覆盖或补充
for model in local {
// 如果 models.dev 没有这个模型,或者本地数据更新,则使用本地数据
if !merged.contains_key(&model.id) {
merged.insert(model.id.clone(), model);
}
}
let mut result: Vec<_> = merged.into_values().collect();
// 按 provider_id 和 display_name 排序
result.sort_by(|a, b| {
a.provider_id
.cmp(&b.provider_id)
.then(a.display_name.cmp(&b.display_name))
});
result
}
/// 从数据库加载模型
async fn load_from_db(&self) -> Result<Vec<EnhancedModelMetadata>, String> {
let (models, sync_rows) = {
let conn = self.db.lock().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare(
"SELECT id, display_name, provider_id, provider_name, family, tier,
capabilities, pricing, limits, status, release_date, is_latest,
description, source, created_at, updated_at
FROM model_registry",
)
.map_err(|e| e.to_string())?;
let models = stmt
.query_map([], |row| {
let capabilities_json: String = row.get(6)?;
let pricing_json: Option<String> = row.get(7)?;
let limits_json: String = row.get(8)?;
let status_str: String = row.get(9)?;
let tier_str: String = row.get(5)?;
let source_str: String = row.get(13)?;
Ok(EnhancedModelMetadata {
id: row.get(0)?,
display_name: row.get(1)?,
provider_id: row.get(2)?,
provider_name: row.get(3)?,
family: row.get(4)?,
tier: tier_str.parse().unwrap_or(ModelTier::Pro),
capabilities: serde_json::from_str(&capabilities_json)
.unwrap_or_default(),
pricing: pricing_json
.and_then(|s| serde_json::from_str(&s).ok()),
limits: serde_json::from_str(&limits_json).unwrap_or_default(),
status: status_str.parse().unwrap_or(ModelStatus::Active),
release_date: row.get(10)?,
is_latest: row.get::<_, i32>(11)? != 0,
description: row.get(12)?,
source: source_str.parse().unwrap_or(ModelSource::Local),
created_at: row.get(14)?,
updated_at: row.get(15)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
// 加载同步状态数据
let mut sync_stmt = conn
.prepare("SELECT key, value FROM model_sync_state")
.map_err(|e| e.to_string())?;
let sync_rows: Vec<(String, String)> = sync_stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
(models, sync_rows)
}; // conn 锁在这里释放
// 更新同步状态(在锁释放后)
{
let mut state = self.sync_state.write().await;
for (key, value) in sync_rows {
match key.as_str() {
"last_sync_at" => {
state.last_sync_at = value.parse().ok();
}
"model_count" => {
state.model_count = value.parse().unwrap_or(0);
}
"last_error" => {
state.last_error = if value.is_empty() { None } else { Some(value) };
}
_ => {}
}
}
}
Ok(models)
}
/// 保存模型到数据库
async fn save_models_to_db(&self, models: &[EnhancedModelMetadata]) -> Result<(), String> {
let conn = self.db.lock().map_err(|e| e.to_string())?;
// 开始事务
conn.execute("BEGIN TRANSACTION", [])
.map_err(|e| e.to_string())?;
// 清空现有数据
conn.execute("DELETE FROM model_registry", [])
.map_err(|e| e.to_string())?;
// 插入新数据
let mut stmt = conn
.prepare(
"INSERT INTO model_registry (
id, display_name, provider_id, provider_name, family, tier,
capabilities, pricing, limits, status, release_date, is_latest,
description, source, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.map_err(|e| e.to_string())?;
for model in models {
let capabilities_json =
serde_json::to_string(&model.capabilities).unwrap_or_default();
let pricing_json = model
.pricing
.as_ref()
.map(|p| serde_json::to_string(p).unwrap_or_default());
let limits_json = serde_json::to_string(&model.limits).unwrap_or_default();
stmt.execute(params![
model.id,
model.display_name,
model.provider_id,
model.provider_name,
model.family,
model.tier.to_string(),
capabilities_json,
pricing_json,
limits_json,
model.status.to_string(),
model.release_date,
model.is_latest as i32,
model.description,
model.source.to_string(),
model.created_at,
model.updated_at,
])
.map_err(|e| e.to_string())?;
}
// 提交事务
conn.execute("COMMIT", []).map_err(|e| e.to_string())?;
tracing::info!("[ModelRegistry] 保存了 {} 个模型到数据库", models.len());
Ok(())
}
/// 保存同步状态
async fn save_sync_state(&self) -> Result<(), String> {
let (last_sync_at, model_count, last_error) = {
let state = self.sync_state.read().await;
(state.last_sync_at, state.model_count, state.last_error.clone())
};
let conn = self.db.lock().map_err(|e| e.to_string())?;
let now = chrono::Utc::now().timestamp();
let mut stmt = conn
.prepare(
"INSERT OR REPLACE INTO model_sync_state (key, value, updated_at)
VALUES (?, ?, ?)",
)
.map_err(|e| e.to_string())?;
if let Some(last_sync) = last_sync_at {
stmt.execute(params!["last_sync_at", last_sync.to_string(), now])
.map_err(|e| e.to_string())?;
}
stmt.execute(params!["model_count", model_count.to_string(), now])
.map_err(|e| e.to_string())?;
if let Some(ref error) = last_error {
stmt.execute(params!["last_error", error, now])
.map_err(|e| e.to_string())?;
}
Ok(())
}
/// 获取所有模型
pub async fn get_all_models(&self) -> Vec<EnhancedModelMetadata> {
self.models_cache.read().await.clone()
}
/// 获取同步状态
pub async fn get_sync_state(&self) -> ModelSyncState {
self.sync_state.read().await.clone()
}
/// 按 Provider 获取模型
pub async fn get_models_by_provider(&self, provider_id: &str) -> Vec<EnhancedModelMetadata> {
self.models_cache
.read()
.await
.iter()
.filter(|m| m.provider_id == provider_id)
.cloned()
.collect()
}
/// 按服务等级获取模型
pub async fn get_models_by_tier(&self, tier: ModelTier) -> Vec<EnhancedModelMetadata> {
self.models_cache
.read()
.await
.iter()
.filter(|m| m.tier == tier)
.cloned()
.collect()
}
/// 搜索模型(简单的模糊匹配)
pub async fn search_models(&self, query: &str, limit: usize) -> Vec<EnhancedModelMetadata> {
let models = self.models_cache.read().await;
if query.is_empty() {
return models.iter().take(limit).cloned().collect();
}
let query_lower = query.to_lowercase();
let mut scored: Vec<(f64, &EnhancedModelMetadata)> = models
.iter()
.filter_map(|m| {
let score = self.calculate_search_score(m, &query_lower);
if score > 0.0 {
Some((score, m))
} else {
None
}
})
.collect();
// 按分数降序排序
scored.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
});
scored
.into_iter()
.take(limit)
.map(|(_, m)| m.clone())
.collect()
}
/// 计算搜索匹配分数
fn calculate_search_score(&self, model: &EnhancedModelMetadata, query: &str) -> f64 {
let mut score = 0.0;
// 精确匹配 ID
if model.id.to_lowercase() == query {
score += 100.0;
} else if model.id.to_lowercase().contains(query) {
score += 50.0;
}
// 显示名称匹配
if model.display_name.to_lowercase().contains(query) {
score += 30.0;
}
// Provider 匹配
if model.provider_name.to_lowercase().contains(query) {
score += 20.0;
}
// 家族匹配
if let Some(family) = &model.family {
if family.to_lowercase().contains(query) {
score += 15.0;
}
}
// 最新版本加分
if model.is_latest {
score += 5.0;
}
// 活跃状态加分
if model.status == ModelStatus::Active {
score += 3.0;
}
score
}
// ========== 用户偏好相关方法 ==========
/// 获取所有用户偏好
pub async fn get_all_preferences(&self) -> Result<Vec<UserModelPreference>, String> {
let conn = self.db.lock().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare(
"SELECT model_id, is_favorite, is_hidden, custom_alias,
usage_count, last_used_at, created_at, updated_at
FROM user_model_preferences",
)
.map_err(|e| e.to_string())?;
let prefs = stmt
.query_map([], |row| {
Ok(UserModelPreference {
model_id: row.get(0)?,
is_favorite: row.get::<_, i32>(1)? != 0,
is_hidden: row.get::<_, i32>(2)? != 0,
custom_alias: row.get(3)?,
usage_count: row.get::<_, i32>(4)? as u32,
last_used_at: row.get(5)?,
created_at: row.get(6)?,
updated_at: row.get(7)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(prefs)
}
/// 切换收藏状态
pub async fn toggle_favorite(&self, model_id: &str) -> Result<bool, String> {
let conn = self.db.lock().map_err(|e| e.to_string())?;
let now = chrono::Utc::now().timestamp();
// 检查是否存在
let exists: bool = conn
.query_row(
"SELECT 1 FROM user_model_preferences WHERE model_id = ?",
params![model_id],
|_| Ok(true),
)
.unwrap_or(false);
if exists {
// 切换状态
conn.execute(
"UPDATE user_model_preferences
SET is_favorite = NOT is_favorite, updated_at = ?
WHERE model_id = ?",
params![now, model_id],
)
.map_err(|e| e.to_string())?;
} else {
// 创建新记录
conn.execute(
"INSERT INTO user_model_preferences
(model_id, is_favorite, is_hidden, usage_count, created_at, updated_at)
VALUES (?, 1, 0, 0, ?, ?)",
params![model_id, now, now],
)
.map_err(|e| e.to_string())?;
}
// 返回新状态
let new_state: bool = conn
.query_row(
"SELECT is_favorite FROM user_model_preferences WHERE model_id = ?",
params![model_id],
|row| Ok(row.get::<_, i32>(0)? != 0),
)
.unwrap_or(false);
Ok(new_state)
}
/// 隐藏模型
pub async fn hide_model(&self, model_id: &str) -> Result<(), String> {
let conn = self.db.lock().map_err(|e| e.to_string())?;
let now = chrono::Utc::now().timestamp();
conn.execute(
"INSERT INTO user_model_preferences
(model_id, is_favorite, is_hidden, usage_count, created_at, updated_at)
VALUES (?, 0, 1, 0, ?, ?)
ON CONFLICT(model_id) DO UPDATE SET is_hidden = 1, updated_at = ?",
params![model_id, now, now, now],
)
.map_err(|e| e.to_string())?;
Ok(())
}
/// 记录模型使用
pub async fn record_usage(&self, model_id: &str) -> Result<(), String> {
let conn = self.db.lock().map_err(|e| e.to_string())?;
let now = chrono::Utc::now().timestamp();
conn.execute(
"INSERT INTO user_model_preferences
(model_id, is_favorite, is_hidden, usage_count, last_used_at, created_at, updated_at)
VALUES (?, 0, 0, 1, ?, ?, ?)
ON CONFLICT(model_id) DO UPDATE SET
usage_count = usage_count + 1,
last_used_at = ?,
updated_at = ?",
params![model_id, now, now, now, now, now],
)
.map_err(|e| e.to_string())?;
Ok(())
}
}
@@ -1,15 +1,5 @@
import React, { useState, useEffect } from "react";
import {
Bot,
ChevronDown,
Check,
Box,
Settings2,
Zap,
Sparkles,
Crown,
Wand2,
} from "lucide-react";
import React, { useState } from "react";
import { Bot, ChevronDown, Check, Box, Settings2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
@@ -17,55 +7,9 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Navbar } from "../styles";
import { PROVIDER_CONFIG } from "../types";
import { cn } from "@/lib/utils";
import {
orchestratorApi,
type ServiceTier,
type PoolStats,
} from "@/lib/api/orchestrator";
// 服务等级配置
const TIER_CONFIG: Record<
ServiceTier,
{
label: string;
description: string;
icon: React.ReactNode;
color: string;
bgColor: string;
}
> = {
mini: {
label: "Mini",
description: "快速响应",
icon: <Zap className="w-3.5 h-3.5" />,
color: "text-green-600 dark:text-green-400",
bgColor: "bg-green-500/10",
},
pro: {
label: "Pro",
description: "均衡性能",
icon: <Sparkles className="w-3.5 h-3.5" />,
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-500/10",
},
max: {
label: "Max",
description: "最强能力",
icon: <Crown className="w-3.5 h-3.5" />,
color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-500/10",
},
};
type SelectionMode = "simple" | "expert";
interface ChatNavbarProps {
providerType: string;
@@ -89,62 +33,10 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
onToggleSettings,
}) => {
const [open, setOpen] = useState(false);
const [mode, setMode] = useState<SelectionMode>("simple");
const [tier, setTier] = useState<ServiceTier>("pro");
const [poolStats, setPoolStats] = useState<PoolStats | null>(null);
const [orchestratorReady, setOrchestratorReady] = useState(false);
// 初始化 orchestrator
useEffect(() => {
const init = async () => {
try {
await orchestratorApi.init();
setOrchestratorReady(true);
const stats = await orchestratorApi.getPoolStats();
setPoolStats(stats);
} catch (err) {
console.warn("Orchestrator 初始化失败,使用专家模式:", err);
setMode("expert");
}
};
init();
}, []);
// 简单模式下选择等级时自动选择模型
const handleTierSelect = async (selectedTier: ServiceTier) => {
setTier(selectedTier);
setOpen(false);
if (!orchestratorReady) return;
try {
const result = await orchestratorApi.selectModel({ tier: selectedTier });
// 映射 orchestrator 的 provider_type 到 PROVIDER_CONFIG 的 key
const providerKey = mapProviderType(result.provider_type);
setProviderType(providerKey);
setModel(result.model_id);
} catch (err) {
console.error("模型选择失败:", err);
}
};
// 映射 provider type
const mapProviderType = (orchestratorType: string): string => {
const mapping: Record<string, string> = {
anthropic: "claude",
openai: "openai",
google: "gemini",
gemini: "gemini",
kiro: "kiro",
codex: "codex",
};
return mapping[orchestratorType.toLowerCase()] || orchestratorType;
};
const selectedProviderLabel =
PROVIDER_CONFIG[providerType]?.label || providerType;
const currentModels = PROVIDER_CONFIG[providerType]?.models || [];
const tierConfig = TIER_CONFIG[tier];
return (
<Navbar>
@@ -170,22 +62,10 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
aria-expanded={open}
className="h-9 px-3 gap-2 font-normal hover:bg-muted text-foreground"
>
{mode === "simple" && orchestratorReady ? (
<>
<span className={tierConfig.color}>{tierConfig.icon}</span>
<span className="font-medium">{tierConfig.label}</span>
<span className="text-muted-foreground text-xs">
({tierConfig.description})
</span>
</>
) : (
<>
<Bot size={16} className="text-primary" />
<span className="font-medium">{selectedProviderLabel}</span>
<span className="text-muted-foreground">/</span>
<span className="text-sm">{model || "Select Model"}</span>
</>
)}
<Bot size={16} className="text-primary" />
<span className="font-medium">{selectedProviderLabel}</span>
<span className="text-muted-foreground">/</span>
<span className="text-sm">{model || "Select Model"}</span>
<ChevronDown className="ml-1 h-3 w-3 text-muted-foreground opacity-50" />
</Button>
</PopoverTrigger>
@@ -193,183 +73,77 @@ export const ChatNavbar: React.FC<ChatNavbarProps> = ({
className="w-[420px] p-0 bg-background/95 backdrop-blur-sm border-border shadow-lg"
align="center"
>
{/* Mode Toggle */}
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/30">
<span className="text-xs font-medium text-muted-foreground">
选择模式
</span>
<div className="flex gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={mode === "simple" ? "secondary" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
onClick={() => setMode("simple")}
disabled={!orchestratorReady}
>
<Wand2 className="w-3 h-3 mr-1" />
简单
</Button>
</TooltipTrigger>
<TooltipContent>Mini/Pro/Max 三档智能选择</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={mode === "expert" ? "secondary" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
onClick={() => setMode("expert")}
>
<Settings2 className="w-3 h-3 mr-1" />
专家
</Button>
</TooltipTrigger>
<TooltipContent>直接选择 Provider 和模型</TooltipContent>
</Tooltip>
{/* Provider/Model Selection */}
<div className="flex h-[300px]">
{/* Left Column: Providers */}
<div className="w-[140px] border-r bg-muted/30 p-2 flex flex-col gap-1 overflow-y-auto">
<div className="text-xs font-semibold text-muted-foreground px-2 py-1.5 mb-1">
Providers
</div>
{Object.entries(PROVIDER_CONFIG).map(([key, config]) => (
<button
key={key}
onClick={() => {
setProviderType(key);
// Auto-select first model if available
if (config.models.length > 0) {
setModel(config.models[0]);
} else {
setModel("");
}
}}
className={cn(
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md transition-colors text-left",
providerType === key
? "bg-primary/10 text-primary font-medium"
: "hover:bg-muted text-muted-foreground hover:text-foreground",
)}
>
{config.label}
{providerType === key && (
<div className="w-1 h-1 rounded-full bg-primary" />
)}
</button>
))}
</div>
{/* Right Column: Models */}
<div className="flex-1 p-2 flex flex-col overflow-hidden">
<div className="text-xs font-semibold text-muted-foreground px-2 py-1.5 mb-1">
Models
</div>
<ScrollArea className="flex-1">
<div className="space-y-1 p-1">
{currentModels.length === 0 ? (
<div className="text-xs text-muted-foreground p-2">
No models available
</div>
) : (
currentModels.map((m) => (
<button
key={m}
onClick={() => {
setModel(m);
setOpen(false);
}}
className={cn(
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md transition-colors text-left group",
model === m
? "bg-accent text-accent-foreground"
: "hover:bg-muted text-muted-foreground hover:text-foreground",
)}
>
{m}
{model === m && (
<Check size={14} className="text-primary" />
)}
</button>
))
)}
</div>
</ScrollArea>
</div>
</div>
{mode === "simple" && orchestratorReady ? (
/* Simple Mode: Tier Selection */
<div className="p-3">
<div className="grid grid-cols-3 gap-2">
{(Object.keys(TIER_CONFIG) as ServiceTier[]).map((t) => {
const config = TIER_CONFIG[t];
const count =
poolStats?.[`${t}_count` as keyof PoolStats] ?? 0;
const isSelected = tier === t;
return (
<button
key={t}
onClick={() => handleTierSelect(t)}
className={cn(
"flex flex-col items-center p-3 rounded-lg border transition-all",
isSelected
? cn(
"border-primary/50",
config.bgColor,
"ring-2 ring-primary/20",
)
: "border-border hover:bg-muted/50",
count === 0 && "opacity-50 cursor-not-allowed",
)}
disabled={count === 0}
>
<span
className={cn(
"mb-1",
isSelected ? config.color : "text-muted-foreground",
)}
>
{config.icon}
</span>
<span
className={cn(
"font-medium text-sm",
isSelected ? config.color : "text-foreground",
)}
>
{config.label}
</span>
<span className="text-xs text-muted-foreground">
{config.description}
</span>
{poolStats && (
<span className="text-xs text-muted-foreground mt-1">
{count} 模型
</span>
)}
</button>
);
})}
</div>
{/* Current Selection Info */}
{model && (
<div className="mt-3 p-2 rounded-md bg-muted/50 text-xs">
<span className="text-muted-foreground">当前模型: </span>
<span className="font-medium">
{selectedProviderLabel} / {model}
</span>
</div>
)}
</div>
) : (
/* Expert Mode: Provider/Model Selection */
<div className="flex h-[300px]">
{/* Left Column: Providers */}
<div className="w-[140px] border-r bg-muted/30 p-2 flex flex-col gap-1 overflow-y-auto">
<div className="text-xs font-semibold text-muted-foreground px-2 py-1.5 mb-1">
Providers
</div>
{Object.entries(PROVIDER_CONFIG).map(([key, config]) => (
<button
key={key}
onClick={() => {
setProviderType(key);
// Auto-select first model if available
if (config.models.length > 0) {
setModel(config.models[0]);
} else {
setModel("");
}
}}
className={cn(
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md transition-colors text-left",
providerType === key
? "bg-primary/10 text-primary font-medium"
: "hover:bg-muted text-muted-foreground hover:text-foreground",
)}
>
{config.label}
{providerType === key && (
<div className="w-1 h-1 rounded-full bg-primary" />
)}
</button>
))}
</div>
{/* Right Column: Models */}
<div className="flex-1 p-2 flex flex-col overflow-hidden">
<div className="text-xs font-semibold text-muted-foreground px-2 py-1.5 mb-1">
Models
</div>
<ScrollArea className="flex-1">
<div className="space-y-1 p-1">
{currentModels.length === 0 ? (
<div className="text-xs text-muted-foreground p-2">
No models available
</div>
) : (
currentModels.map((m) => (
<button
key={m}
onClick={() => {
setModel(m);
setOpen(false);
}}
className={cn(
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md transition-colors text-left group",
model === m
? "bg-accent text-accent-foreground"
: "hover:bg-muted text-muted-foreground hover:text-foreground",
)}
>
{m}
{model === m && (
<Check size={14} className="text-primary" />
)}
</button>
))
)}
</div>
</ScrollArea>
</div>
</div>
)}
</PopoverContent>
</Popover>
</div>
@@ -137,6 +137,18 @@ export function useAgentChat() {
savePersisted("agent_pref_model", model);
}, [model]);
// 当 provider 改变时,检查当前模型是否兼容
// 如果不兼容,自动切换到新 provider 的第一个模型
useEffect(() => {
const currentProviderModels = providerConfig[providerType]?.models || [];
if (currentProviderModels.length > 0 && !currentProviderModels.includes(model)) {
console.log(
`[useAgentChat] 模型 ${model} 不在 ${providerType} 支持列表中,自动切换到 ${currentProviderModels[0]}`
);
setModel(currentProviderModels[0]);
}
}, [providerType, providerConfig, model]);
useEffect(() => {
saveTransient("agent_curr_sessionId", sessionId);
}, [sessionId]);
+87 -10
View File
@@ -56,42 +56,91 @@ export const PROVIDER_CONFIG: Record<
claude: {
label: "Claude",
models: [
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-opus-4-1",
"claude-opus-4-1-20250805",
"claude-opus-4-20250514",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
"claude-3-7-sonnet-20250219",
"claude-3-5-haiku-20241022",
],
},
anthropic: {
label: "Anthropic",
models: [
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-opus-4-1",
"claude-opus-4-1-20250805",
"claude-opus-4-20250514",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
"claude-3-7-sonnet-20250219",
"claude-3-5-haiku-20241022",
],
},
kiro: {
label: "Kiro",
models: ["claude-sonnet-4-5-20250929", "claude-sonnet-4-20250514"],
models: [
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-haiku-4-5",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-20250514",
"claude-3-7-sonnet-20250219",
],
},
openai: {
label: "OpenAI",
models: [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
"o1",
"o1-mini",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1",
"gpt-5.1-codex-max",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5",
"gpt-5-pro",
"gpt-5-codex",
"gpt-5-codex-mini",
"gpt-5-mini",
"gpt-5-nano",
"o4-mini",
"o3",
"o3-mini",
"o1",
"o1-mini",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o",
"gpt-4o-mini",
],
},
gemini: {
label: "Gemini",
models: ["gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash"],
models: [
"gemini-3-pro-preview",
"gemini-3-flash-preview",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
],
},
qwen: {
label: "通义千问",
models: ["qwen-max", "qwen-plus", "qwen-turbo"],
models: ["qwen3-coder-plus", "qwen3-coder-flash"],
},
codex: {
label: "Codex",
@@ -99,11 +148,38 @@ export const PROVIDER_CONFIG: Record<
},
claude_oauth: {
label: "Claude OAuth",
models: ["claude-sonnet-4-5-20250929", "claude-3-5-sonnet-20241022"],
models: [
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
"claude-sonnet-4-20250514",
"claude-haiku-4-5",
"claude-3-7-sonnet-20250219",
],
},
iflow: {
label: "iFlow",
models: [],
models: [
"tstars2.0",
"qwen3-coder-plus",
"qwen3-max",
"qwen3-vl-plus",
"qwen3-max-preview",
"kimi-k2-0905",
"glm-4.7",
"glm-4.6",
"kimi-k2",
"kimi-k2-thinking",
"deepseek-v3.2-chat",
"deepseek-v3.2-reasoner",
"deepseek-v3.2",
"deepseek-v3.1",
"deepseek-r1",
"deepseek-v3",
"minimax-m2.1",
"minimax-m2",
],
},
antigravity: {
label: "Antigravity",
@@ -111,6 +187,7 @@ export const PROVIDER_CONFIG: Record<
"gemini-3-pro-preview",
"gemini-3-pro-image-preview",
"gemini-3-flash-preview",
"gemini-2.5-flash",
"gemini-2.5-computer-use-preview-10-2025",
"gemini-claude-sonnet-4-5",
"gemini-claude-sonnet-4-5-thinking",
+6 -1
View File
@@ -9,6 +9,7 @@ import {
} from "lucide-react";
import { LogsTab } from "./LogsTab";
import { RoutesTab } from "./RoutesTab";
import { EnhancedModelsTab } from "./EnhancedModelsTab";
import { ProviderIcon } from "@/icons/providers";
import {
startServer,
@@ -40,7 +41,7 @@ interface TestState {
httpStatus?: number;
}
type TabId = "server" | "routes" | "logs";
type TabId = "server" | "routes" | "models" | "logs";
// 可用的 Provider 信息(合并 OAuth 凭证池和 API Key Provider)
interface AvailableProvider {
@@ -670,6 +671,7 @@ export function ApiServerPage() {
{[
{ id: "server" as TabId, name: "服务器控制" },
{ id: "routes" as TabId, name: "路由端点" },
{ id: "models" as TabId, name: "模型列表" },
{ id: "logs" as TabId, name: "系统日志" },
].map((tab) => (
<button
@@ -1017,6 +1019,9 @@ export function ApiServerPage() {
{/* Routes Tab */}
{activeTab === "routes" && <RoutesTab />}
{/* Models Tab */}
{activeTab === "models" && <EnhancedModelsTab />}
{/* Logs Tab */}
{activeTab === "logs" && <LogsTab />}
</div>
@@ -0,0 +1,370 @@
/**
* 增强版模型列表页面
*
* 使用 model_registry 数据,支持搜索、收藏、分组等功能
*/
import { useState } from "react";
import {
Cpu,
RefreshCw,
Copy,
Check,
Search,
Star,
Clock,
Filter,
Eye,
Wrench,
Brain,
DollarSign,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useModelRegistry } from "@/hooks/useModelRegistry";
import type { EnhancedModelMetadata, ModelTier } from "@/lib/types/modelRegistry";
export function EnhancedModelsTab() {
const {
models,
preferences,
loading,
error,
lastSyncAt,
refresh,
search,
toggleFavorite,
groupedByProvider,
} = useModelRegistry();
const [searchQuery, setSearchQuery] = useState("");
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [selectedTier, setSelectedTier] = useState<ModelTier | null>(null);
const [copied, setCopied] = useState<string | null>(null);
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
// 搜索和过滤
const filteredModels = (() => {
let result = searchQuery ? search(searchQuery) : models;
if (selectedProvider) {
result = result.filter((m) => m.provider_id === selectedProvider);
}
if (selectedTier) {
result = result.filter((m) => m.tier === selectedTier);
}
if (showFavoritesOnly) {
result = result.filter((m) => preferences.get(m.id)?.is_favorite);
}
return result;
})();
const providers = Array.from(groupedByProvider.keys());
const copyModelId = (id: string) => {
navigator.clipboard.writeText(id);
setCopied(id);
setTimeout(() => setCopied(null), 2000);
};
const formatSyncTime = (timestamp: number | null) => {
if (!timestamp) return "从未同步";
return new Date(timestamp * 1000).toLocaleString("zh-CN");
};
return (
<div className="space-y-6">
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 dark:bg-red-950 p-4 text-red-700 dark:text-red-300">
{error}
</div>
)}
{/* 头部信息 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>上次同步: {formatSyncTime(lastSyncAt)}</span>
</div>
<button
onClick={refresh}
disabled={loading}
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
刷新
</button>
</div>
{/* 搜索和过滤 */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="搜索模型名称、ID、Provider..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
/>
</div>
<button
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
className={cn(
"flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors",
showFavoritesOnly
? "bg-yellow-100 border-yellow-300 text-yellow-700 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-300"
: "hover:bg-muted"
)}
>
<Star className={cn("h-4 w-4", showFavoritesOnly && "fill-yellow-500 text-yellow-500")} />
收藏
</button>
</div>
{/* Provider 过滤 */}
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedProvider(null)}
className={cn(
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
!selectedProvider ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
)}
>
全部 ({models.length})
</button>
{providers.map((providerId) => {
const providerModels = groupedByProvider.get(providerId) || [];
const providerName = providerModels[0]?.provider_name || providerId;
return (
<button
key={providerId}
onClick={() => setSelectedProvider(selectedProvider === providerId ? null : providerId)}
className={cn(
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
selectedProvider === providerId ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
)}
>
{providerName} ({providerModels.length})
</button>
);
})}
</div>
{/* Tier 过滤 */}
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">等级:</span>
{(["mini", "pro", "max"] as ModelTier[]).map((tier) => (
<button
key={tier}
onClick={() => setSelectedTier(selectedTier === tier ? null : tier)}
className={cn(
"rounded-lg px-3 py-1 text-xs font-medium transition-colors",
selectedTier === tier ? getTierButtonActiveClass(tier) : "bg-muted hover:bg-muted/80"
)}
>
{tier.toUpperCase()}
</button>
))}
</div>
</div>
{/* 模型列表 */}
<div className="rounded-lg border bg-card">
<div className="border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className="font-medium">模型列表</span>
<span className="text-sm text-muted-foreground">{filteredModels.length} 个模型</span>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : filteredModels.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Cpu className="h-12 w-12 mb-2 opacity-50" />
<p>暂无模型数据</p>
</div>
) : (
<div className="divide-y max-h-[600px] overflow-y-auto">
{filteredModels.map((model) => (
<ModelRow
key={model.id}
model={model}
isFavorite={preferences.get(model.id)?.is_favorite || false}
usageCount={preferences.get(model.id)?.usage_count || 0}
copied={copied === model.id}
onCopy={() => copyModelId(model.id)}
onToggleFavorite={() => toggleFavorite(model.id)}
/>
))}
</div>
)}
</div>
{/* 使用说明 */}
<div className="rounded-lg border bg-card p-4">
<h3 className="mb-2 font-semibold">使用说明</h3>
<div className="space-y-2 text-sm text-muted-foreground">
<p>• 模型数据来自 models.dev API 和本地配置</p>
<p>• 点击星标可收藏常用模型,收藏的模型会优先显示</p>
<p>• 支持按 Provider、服务等级筛选模型</p>
</div>
</div>
</div>
);
}
/** 单个模型行 */
function ModelRow({
model,
isFavorite,
usageCount,
copied,
onCopy,
onToggleFavorite,
}: {
model: EnhancedModelMetadata;
isFavorite: boolean;
usageCount: number;
copied: boolean;
onCopy: () => void;
onToggleFavorite: () => void;
}) {
return (
<div className="flex items-center justify-between px-4 py-3 hover:bg-muted/50">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Cpu className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<code className="font-medium truncate">{model.id}</code>
<TierBadge tier={model.tier} />
{model.is_latest && (
<span className="text-xs px-1.5 py-0.5 rounded bg-primary/10 text-primary">
最新
</span>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span>{model.provider_name}</span>
{model.limits.context_length && (
<>
<span>·</span>
<span>{formatContextLength(model.limits.context_length)}</span>
</>
)}
{usageCount > 0 && (
<>
<span>·</span>
<span>使用 {usageCount} 次</span>
</>
)}
</div>
</div>
</div>
{/* 能力图标 */}
<div className="flex items-center gap-1 mr-3">
{model.capabilities.vision && (
<span title="支持视觉">
<Eye className="h-3.5 w-3.5 text-blue-500" />
</span>
)}
{model.capabilities.tools && (
<span title="支持工具">
<Wrench className="h-3.5 w-3.5 text-green-500" />
</span>
)}
{model.capabilities.reasoning && (
<span title="支持推理">
<Brain className="h-3.5 w-3.5 text-purple-500" />
</span>
)}
</div>
{/* 定价 */}
{model.pricing && model.pricing.input_per_million && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-3">
<DollarSign className="h-3 w-3" />
<span>{model.pricing.input_per_million.toFixed(2)}</span>
</div>
)}
{/* 操作按钮 */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("[EnhancedModelsTab] Toggle favorite:", model.id);
onToggleFavorite();
}}
className="p-2 rounded-lg hover:bg-muted transition-colors cursor-pointer"
title={isFavorite ? "取消收藏" : "收藏"}
>
<Star
className={cn(
"h-5 w-5",
isFavorite ? "text-yellow-500 fill-yellow-500" : "text-muted-foreground hover:text-yellow-400"
)}
/>
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onCopy();
}}
className="p-2 rounded-lg hover:bg-muted transition-colors cursor-pointer"
title="复制模型 ID"
>
{copied ? (
<Check className="h-5 w-5 text-green-500" />
) : (
<Copy className="h-5 w-5 text-muted-foreground hover:text-foreground" />
)}
</button>
</div>
</div>
);
}
/** 服务等级徽章 */
function TierBadge({ tier }: { tier: string }) {
const config = {
mini: { label: "Mini", color: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300" },
pro: { label: "Pro", color: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300" },
max: { label: "Max", color: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" },
}[tier] || { label: tier, color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" };
return (
<span className={cn("text-xs px-1.5 py-0.5 rounded", config.color)}>
{config.label}
</span>
);
}
/** 获取 Tier 按钮激活状态的样式 */
function getTierButtonActiveClass(tier: ModelTier): string {
const classes = {
mini: "bg-green-100 text-green-700 border-green-300 dark:bg-green-900 dark:text-green-300 dark:border-green-700",
pro: "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700",
max: "bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900 dark:text-purple-300 dark:border-purple-700",
};
return classes[tier] || "bg-primary text-primary-foreground";
}
/** 格式化上下文长度 */
function formatContextLength(length: number): string {
if (length >= 1000000) {
return `${(length / 1000000).toFixed(1)}M`;
}
if (length >= 1000) {
return `${(length / 1000).toFixed(0)}K`;
}
return String(length);
}
@@ -0,0 +1,380 @@
/**
* 增强版模型列表组件
*
* 使用 model_registry 数据,支持搜索、收藏、分组等功能
*/
import { useState, useMemo } from "react";
import {
Check,
AlertCircle,
Loader2,
Star,
Search,
ChevronDown,
ChevronRight,
Eye,
Wrench,
Brain,
DollarSign,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry";
interface EnhancedModelListProps {
/** 模型列表 */
models: EnhancedModelMetadata[];
/** 选中的模型 ID */
selectedModelId?: string;
/** 选择模型回调 */
onSelectModel?: (model: EnhancedModelMetadata) => void;
/** 收藏模型回调 */
onToggleFavorite?: (modelId: string) => void;
/** 收藏的模型 ID 集合 */
favorites?: Set<string>;
/** 是否加载中 */
loading?: boolean;
/** 错误信息 */
error?: string | null;
/** 是否按 Provider 分组 */
groupByProvider?: boolean;
/** 是否显示搜索框 */
showSearch?: boolean;
/** 是否显示定价信息 */
showPricing?: boolean;
/** 自定义类名 */
className?: string;
}
export function EnhancedModelList({
models,
selectedModelId,
onSelectModel,
onToggleFavorite,
favorites = new Set(),
loading = false,
error = null,
groupByProvider = true,
showSearch = true,
showPricing = false,
className,
}: EnhancedModelListProps) {
const [searchQuery, setSearchQuery] = useState("");
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
new Set(["favorites"])
);
// 过滤模型
const filteredModels = useMemo(() => {
if (!searchQuery.trim()) return models;
const query = searchQuery.toLowerCase();
return models.filter(
(m) =>
m.id.toLowerCase().includes(query) ||
m.display_name.toLowerCase().includes(query) ||
m.provider_name.toLowerCase().includes(query) ||
m.family?.toLowerCase().includes(query)
);
}, [models, searchQuery]);
// 按 Provider 分组
const groupedModels = useMemo(() => {
if (!groupByProvider) {
return { all: filteredModels };
}
const groups: Record<string, EnhancedModelMetadata[]> = {};
// 先添加收藏组
const favoriteModels = filteredModels.filter((m) => favorites.has(m.id));
if (favoriteModels.length > 0) {
groups["favorites"] = favoriteModels;
}
// 按 Provider 分组
for (const model of filteredModels) {
if (!groups[model.provider_id]) {
groups[model.provider_id] = [];
}
groups[model.provider_id].push(model);
}
return groups;
}, [filteredModels, groupByProvider, favorites]);
// 切换分组展开状态
const toggleGroup = (groupId: string) => {
setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(groupId)) {
next.delete(groupId);
} else {
next.add(groupId);
}
return next;
});
};
if (loading) {
return (
<div className={cn("flex items-center justify-center py-8", className)}>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">加载模型列表...</span>
</div>
);
}
if (error) {
return (
<div
className={cn(
"flex items-center justify-center py-8 text-destructive",
className
)}
>
<AlertCircle className="h-5 w-5 mr-2" />
<span>{error}</span>
</div>
);
}
if (models.length === 0) {
return (
<div className={cn("text-center py-8 text-muted-foreground", className)}>
<p>暂无可用模型</p>
<p className="text-sm mt-1">请等待模型数据加载</p>
</div>
);
}
return (
<div className={cn("space-y-3", className)}>
{/* 搜索框 */}
{showSearch && (
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="搜索模型..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
/>
</div>
)}
{/* 模型列表 */}
<div className="space-y-2">
{Object.entries(groupedModels).map(([groupId, groupModels]) => {
const isExpanded = expandedGroups.has(groupId);
const groupName = getGroupName(groupId, groupModels[0]);
return (
<div key={groupId} className="border rounded-lg overflow-hidden">
{/* 分组头部 */}
{groupByProvider && (
<button
type="button"
onClick={() => toggleGroup(groupId)}
className="w-full flex items-center justify-between px-3 py-2 bg-muted/50 hover:bg-muted transition-colors"
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
{groupId === "favorites" && (
<Star className="h-4 w-4 text-yellow-500 fill-yellow-500" />
)}
<span className="font-medium text-sm">{groupName}</span>
</div>
<span className="text-xs text-muted-foreground">
{groupModels.length} 个模型
</span>
</button>
)}
{/* 模型列表 */}
{(!groupByProvider || isExpanded) && (
<div className="divide-y">
{groupModels.map((model) => (
<ModelItem
key={model.id}
model={model}
isSelected={model.id === selectedModelId}
isFavorite={favorites.has(model.id)}
onSelect={() => onSelectModel?.(model)}
onToggleFavorite={() => onToggleFavorite?.(model.id)}
showPricing={showPricing}
/>
))}
</div>
)}
</div>
);
})}
</div>
{/* 无搜索结果 */}
{filteredModels.length === 0 && searchQuery && (
<div className="text-center py-8 text-muted-foreground">
<p>未找到匹配的模型</p>
<p className="text-sm mt-1">尝试其他搜索词</p>
</div>
)}
</div>
);
}
/** 单个模型项 */
function ModelItem({
model,
isSelected,
isFavorite,
onSelect,
onToggleFavorite,
showPricing,
}: {
model: EnhancedModelMetadata;
isSelected: boolean;
isFavorite: boolean;
onSelect: () => void;
onToggleFavorite: () => void;
showPricing: boolean;
}) {
return (
<div
className={cn(
"flex items-center justify-between px-3 py-2.5 hover:bg-muted/30 transition-colors cursor-pointer",
isSelected && "bg-primary/5"
)}
onClick={onSelect}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
{/* 选中指示器 */}
<div
className={cn(
"w-4 h-4 rounded-full border-2 flex-shrink-0 flex items-center justify-center",
isSelected ? "border-primary bg-primary" : "border-muted-foreground"
)}
>
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
</div>
{/* 模型信息 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{model.display_name}
</span>
{model.is_latest && (
<span className="text-xs px-1.5 py-0.5 rounded bg-primary/10 text-primary">
最新
</span>
)}
<TierBadge tier={model.tier} />
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{model.id}</span>
{model.limits.context_length && (
<>
<span>·</span>
<span>{formatContextLength(model.limits.context_length)}</span>
</>
)}
</div>
</div>
</div>
{/* 能力标签和操作 */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* 能力图标 */}
<div className="flex items-center gap-1">
{model.capabilities.vision && (
<span title="支持视觉">
<Eye className="h-3.5 w-3.5 text-blue-500" />
</span>
)}
{model.capabilities.tools && (
<span title="支持工具">
<Wrench className="h-3.5 w-3.5 text-green-500" />
</span>
)}
{model.capabilities.reasoning && (
<span title="支持推理">
<Brain className="h-3.5 w-3.5 text-purple-500" />
</span>
)}
</div>
{/* 定价 */}
{showPricing && model.pricing && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<DollarSign className="h-3 w-3" />
<span>
{model.pricing.input_per_million?.toFixed(2) || "?"}
</span>
</div>
)}
{/* 收藏按钮 */}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onToggleFavorite();
}}
className="p-1 rounded hover:bg-muted transition-colors"
title={isFavorite ? "取消收藏" : "收藏"}
>
<Star
className={cn(
"h-4 w-4",
isFavorite
? "text-yellow-500 fill-yellow-500"
: "text-muted-foreground"
)}
/>
</button>
</div>
</div>
);
}
/** 服务等级徽章 */
function TierBadge({ tier }: { tier: string }) {
const config = {
mini: { label: "Mini", color: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300" },
pro: { label: "Pro", color: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300" },
max: { label: "Max", color: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" },
}[tier] || { label: tier, color: "bg-gray-100 text-gray-700" };
return (
<span className={cn("text-xs px-1.5 py-0.5 rounded", config.color)}>
{config.label}
</span>
);
}
/** 获取分组名称 */
function getGroupName(
groupId: string,
firstModel: EnhancedModelMetadata
): string {
if (groupId === "favorites") return "收藏";
if (groupId === "all") return "全部模型";
return firstModel?.provider_name || groupId;
}
/** 格式化上下文长度 */
function formatContextLength(length: number): string {
if (length >= 1000000) {
return `${(length / 1000000).toFixed(1)}M`;
}
if (length >= 1000) {
return `${(length / 1000).toFixed(0)}K`;
}
return String(length);
}
+2 -2
View File
@@ -64,11 +64,11 @@ export function ModelList({
return (
<div className={cn("space-y-2", className)}>
{models.map((model) => {
const isSelected = model.model_id === selectedModelId;
const isSelected = model.id === selectedModelId;
return (
<button
key={`${model.model_id}-${model.credential_id}`}
key={`${model.id}-${model.credential_id}`}
type="button"
onClick={() => onSelectModel?.(model)}
className={cn(
@@ -165,7 +165,7 @@ export function ModelSelector({
{mode === "expert" && (
<ModelList
models={models}
selectedModelId={selectedModel?.model_id}
selectedModelId={selectedModel?.id}
onSelectModel={handleModelSelect}
loading={modelsLoading}
error={modelsError}
+117 -73
View File
@@ -71,6 +71,10 @@ interface RecommendedOAuthPlugin {
downloadUrl: string;
/** 标签 */
tags?: string[];
/** 是否推荐 */
recommended?: boolean;
/** 是否可安装(false 表示即将推出) */
available?: boolean;
}
/**
@@ -87,11 +91,13 @@ const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
type: "git_hub",
owner: "aiclientproxy",
repo: "kiro-provider",
version: "v0.2.0",
version: "v0.3.0",
},
downloadUrl:
"https://github.com/aiclientproxy/kiro-provider/releases/download/v0.2.0/kiro-provider-plugin.zip",
"https://github.com/aiclientproxy/kiro-provider/releases/download/v0.3.0/kiro-provider-plugin.zip",
tags: ["anthropic", "免费"],
recommended: true,
available: true,
},
{
id: "antigravity-provider",
@@ -104,11 +110,13 @@ const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
type: "git_hub",
owner: "aiclientproxy",
repo: "antigravity-provider",
version: "v0.1.0",
version: "v0.4.0",
},
downloadUrl:
"https://github.com/aiclientproxy/antigravity-provider/releases/download/v0.1.0/antigravity-provider-plugin.zip",
"https://github.com/aiclientproxy/antigravity-provider/releases/download/v0.4.0/antigravity-provider-plugin.zip",
tags: ["gemini", "claude", "免费"],
recommended: true,
available: true,
},
{
id: "claude-provider",
@@ -120,11 +128,13 @@ const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
type: "git_hub",
owner: "aiclientproxy",
repo: "claude-provider",
version: "v0.1.0",
version: "v0.2.0",
},
downloadUrl:
"https://github.com/aiclientproxy/claude-provider/releases/download/v0.1.0/claude-provider-plugin.zip",
"https://github.com/aiclientproxy/claude-provider/releases/download/v0.2.0/claude-provider-plugin.zip",
tags: ["anthropic", "官方"],
recommended: false,
available: true,
},
{
id: "droid-provider",
@@ -137,11 +147,13 @@ const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
type: "git_hub",
owner: "aiclientproxy",
repo: "droid-provider",
version: "v0.1.0",
version: "v0.2.0",
},
downloadUrl:
"https://github.com/aiclientproxy/droid-provider/releases/download/v0.1.0/droid-provider-plugin.zip",
"https://github.com/aiclientproxy/droid-provider/releases/download/v0.2.0/droid-provider-plugin.zip",
tags: ["anthropic", "openai"],
recommended: false,
available: true,
},
{
id: "gemini-provider",
@@ -153,11 +165,13 @@ const recommendedOAuthPlugins: RecommendedOAuthPlugin[] = [
type: "git_hub",
owner: "aiclientproxy",
repo: "gemini-provider",
version: "v0.1.0",
version: "v0.2.0",
},
downloadUrl:
"https://github.com/aiclientproxy/gemini-provider/releases/download/v0.1.0/gemini-provider-plugin.zip",
"https://github.com/aiclientproxy/gemini-provider/releases/download/v0.2.0/gemini-provider-plugin.zip",
tags: ["gemini", "API Key"],
recommended: false,
available: true,
},
];
@@ -181,13 +195,17 @@ const RecommendedPluginCard: React.FC<{
};
return (
<Card className="relative transition-shadow hover:shadow-md">
<Badge
className="absolute -top-2 -right-2 bg-green-500"
variant="default"
>
推荐
</Badge>
<Card
className={`relative transition-shadow hover:shadow-md ${!plugin.available ? "opacity-70" : ""}`}
>
{plugin.recommended && (
<Badge
className="absolute -top-2 -right-2 bg-green-500"
variant="default"
>
推荐
</Badge>
)}
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
@@ -220,19 +238,25 @@ const RecommendedPluginCard: React.FC<{
</div>
</CardContent>
<CardFooter className="pt-2">
<Button className="w-full" onClick={onInstall} disabled={installing}>
{installing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
安装中...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
一键安装
</>
)}
</Button>
{plugin.available ? (
<Button className="w-full" onClick={onInstall} disabled={installing}>
{installing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
安装中...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
一键安装
</>
)}
</Button>
) : (
<Button className="w-full" variant="secondary" disabled>
即将推出
</Button>
)}
</CardFooter>
</Card>
);
@@ -433,7 +457,10 @@ const PluginDetailView: React.FC<{
fallback={
<div className="flex flex-col items-center justify-center p-8 text-muted-foreground">
<Package className="h-12 w-12 mb-4 opacity-50" />
<p>该插件没有提供 UI</p>
<p className="text-center">该插件暂无 UI 界面</p>
<p className="text-center text-sm mt-2 opacity-70">
请通过凭证池页面的「OAuth 凭证」标签管理此插件的凭证
</p>
</div>
}
/>
@@ -678,16 +705,53 @@ export const OAuthPluginTab: React.FC = () => {
</Card>
)}
{/* 推荐插件 */}
{/* 已安装插件 - 放在上面 */}
{filteredPlugins.length > 0 && (
<div className="rounded-lg border bg-card">
<div className="p-4 border-b">
<h4 className="font-semibold flex items-center gap-2">
<Package className="h-4 w-4" />
已安装插件
</h4>
<p className="text-sm text-muted-foreground mt-1">
点击卡片进入插件详情,管理凭证
</p>
</div>
<div className="p-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredPlugins.map((plugin) => (
<PluginCard
key={plugin.id}
plugin={plugin}
update={updates.find((u) => u.pluginId === plugin.id)}
onSelect={() => setSelectedPluginId(plugin.id)}
onToggle={() => handleToggle(plugin)}
onUninstall={() => {
setPluginToUninstall(plugin.id);
setUninstallDialogOpen(true);
}}
onUpdate={
updates.find((u) => u.pluginId === plugin.id)
? () => update(plugin.id)
: undefined
}
/>
))}
</div>
</div>
</div>
)}
{/* 推荐插件 - 放在下面 */}
{uninstalledRecommendedPlugins.length > 0 && (
<div className="rounded-lg border bg-card">
<div className="p-4 border-b">
<h4 className="font-semibold flex items-center gap-2">
<Download className="h-4 w-4" />
推荐 OAuth Provider 插件
可安装的 OAuth Provider 插件
</h4>
<p className="text-sm text-muted-foreground mt-1">
一键安装推荐的 OAuth Provider 插件,快速扩展支持的 AI 服务
一键安装 OAuth Provider 插件,快速扩展支持的 AI 服务
</p>
</div>
<div className="p-4">
@@ -705,45 +769,25 @@ export const OAuthPluginTab: React.FC = () => {
</div>
)}
{/* 插件列表 */}
{filteredPlugins.length === 0 ? (
<div className="flex flex-col items-center justify-center p-16 border rounded-lg border-dashed">
<Package className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-2">
{searchQuery ? "没有找到匹配的插件" : "暂无已安装的插件"}
</p>
{!searchQuery && (
<Button
variant="outline"
onClick={() => setInstallDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-1" />
安装第一个插件
</Button>
)}
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredPlugins.map((plugin) => (
<PluginCard
key={plugin.id}
plugin={plugin}
update={updates.find((u) => u.pluginId === plugin.id)}
onSelect={() => setSelectedPluginId(plugin.id)}
onToggle={() => handleToggle(plugin)}
onUninstall={() => {
setPluginToUninstall(plugin.id);
setUninstallDialogOpen(true);
}}
onUpdate={
updates.find((u) => u.pluginId === plugin.id)
? () => update(plugin.id)
: undefined
}
/>
))}
</div>
)}
{/* 空状态 - 没有已安装插件且没有推荐插件 */}
{filteredPlugins.length === 0 &&
uninstalledRecommendedPlugins.length === 0 && (
<div className="flex flex-col items-center justify-center p-16 border rounded-lg border-dashed">
<Package className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-2">
{searchQuery ? "没有找到匹配的插件" : "暂无可用的插件"}
</p>
{!searchQuery && (
<Button
variant="outline"
onClick={() => setInstallDialogOpen(true)}
>
<Plus className="h-4 w-4 mr-1" />
手动安装插件
</Button>
)}
</div>
)}
{/* 安装对话框 */}
<InstallPluginDialog
+15 -137
View File
@@ -31,12 +31,11 @@ import { AddCredentialModal } from "./AddCredentialModal";
import { EditCredentialModal } from "./EditCredentialModal";
import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { getConfig, saveConfig, Config } from "@/hooks/useTauri";
import { VertexAISection } from "./VertexAISection";
import { AmpConfigSection } from "./AmpConfigSection";
import { getConfig } from "@/hooks/useTauri";
import { ProviderIcon } from "@/icons/providers";
import { ApiKeyProviderSection, AddCustomProviderModal } from "./api-key";
import { OAuthPluginTab } from "./OAuthPluginTab";
import { RelayProvidersSection } from "./RelayProvidersSection";
import type { AddCustomProviderRequest } from "@/lib/api/apiKeyProvider";
import {
getLocalKiroCredentialUuid,
@@ -60,8 +59,8 @@ const oauthProviderTypes: PoolProviderType[] = [
"iflow",
];
// 配置类型 tab(非凭证池,存储在配置文件中)
type ConfigTabType = "vertex" | "amp";
// 配置类型 tab(非凭证池)
type ConfigTabType = "connect";
// 所有 tab 类型
type TabType = PoolProviderType | ConfigTabType;
@@ -79,18 +78,13 @@ const providerLabels: Record<PoolProviderType, string> = {
gemini_api_key: "Gemini",
};
const configTabLabels: Record<ConfigTabType, string> = {
vertex: "Vertex AI",
amp: "Amp CLI",
};
// 判断是否为配置类型 tab
const isConfigTab = (tab: TabType): tab is ConfigTabType => {
return ["vertex", "amp"].includes(tab);
return tab === "connect";
};
// 分类类型
type CategoryType = "oauth" | "apikey" | "plugins" | "config";
type CategoryType = "oauth" | "apikey" | "plugins" | "connect";
export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
(_props, ref) => {
@@ -137,45 +131,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
// Kiro 本地活跃凭证 UUID
const [localActiveUuid, setLocalActiveUuid] = useState<string | null>(null);
// 配置 tab 相关状态
const [config, setConfig] = useState<Config | null>(null);
const [configLoading, setConfigLoading] = useState(false);
const [configSaving, setConfigSaving] = useState(false);
// 加载配置
const loadConfig = async () => {
setConfigLoading(true);
try {
const c = await getConfig();
setConfig(c);
} catch (e) {
console.error("Failed to load config:", e);
showError("加载配置失败", "config");
}
setConfigLoading(false);
};
// 保存配置
const handleSaveConfig = async () => {
if (!config) return;
setConfigSaving(true);
try {
await saveConfig(config);
showSuccess("配置已保存");
} catch (e) {
showError(e instanceof Error ? e.message : String(e), "config");
}
setConfigSaving(false);
};
// 切换到配置 tab 时加载配置
useEffect(() => {
if (isConfigTab(activeTab)) {
loadConfig();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab]);
// 获取本地活跃的 Kiro 凭证 UUID
const fetchLocalActiveUuid = async () => {
try {
@@ -443,17 +398,17 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</button>
<button
onClick={() => {
setActiveCategory("config");
setActiveTab("vertex");
setActiveCategory("connect");
setActiveTab("connect");
}}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
activeCategory === "config"
activeCategory === "connect"
? "border-primary bg-primary/10 text-primary"
: "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
data-testid="config-category-tab"
data-testid="connect-category-tab"
>
其他配置
Connect
</button>
</div>
@@ -497,26 +452,10 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</div>
)}
{/* 其他配置分类 - 配置 Tab 选择 */}
{activeCategory === "config" && (
<div className="flex flex-wrap gap-2">
{(["vertex", "amp"] as const).map((tabId) => {
const isActive = activeTab === tabId;
return (
<button
key={tabId}
onClick={() => setActiveTab(tabId)}
className={`min-w-[120px] px-3 py-2 rounded-lg border text-sm font-medium transition-all ${
isActive
? "border-primary bg-primary/10 text-primary shadow-sm"
: "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
data-testid={`config-tab-${tabId}`}
>
{configTabLabels[tabId]}
</button>
);
})}
{/* Connect 分类 - 中转商列表 */}
{activeCategory === "connect" && (
<div className="min-h-[400px]" data-testid="connect-section">
<RelayProvidersSection />
</div>
)}
@@ -539,67 +478,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
</div>
)}
{/* 配置 Tab 内容 */}
{activeCategory === "config" &&
isConfigTab(activeTab) &&
(configLoading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : config ? (
<div className="space-y-4">
{activeTab === "vertex" && (
<>
<VertexAISection
entries={config.credential_pool?.vertex_api_keys ?? []}
onChange={(entries) =>
setConfig({
...config,
credential_pool: {
...config.credential_pool,
vertex_api_keys: entries,
},
})
}
/>
{(config.credential_pool?.vertex_api_keys?.length ?? 0) >
0 && (
<button
onClick={handleSaveConfig}
disabled={configSaving}
className="w-full px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-50"
>
{configSaving ? "保存中..." : "保存配置"}
</button>
)}
</>
)}
{activeTab === "amp" && (
<AmpConfigSection
config={
config.ampcode ?? {
upstream_url: null,
model_mappings: [],
restrict_management_to_localhost: false,
}
}
onChange={(ampConfig) =>
setConfig({
...config,
ampcode: ampConfig,
})
}
onSave={handleSaveConfig}
/>
)}
</div>
) : (
<div className="flex items-center justify-center py-12 text-muted-foreground">
加载配置失败
</div>
))}
{/* OAuth 凭证内容 - 卡片布局 */}
{activeCategory === "oauth" &&
!isConfigTab(activeTab) &&
+6 -2
View File
@@ -13,11 +13,13 @@
| `EditCredentialModal.tsx` | 编辑凭证模态框组件 |
| `ErrorDisplay.tsx` | 错误显示组件 |
| `UsageDisplay.tsx` | 用量显示组件 |
| `RelayProvidersSection.tsx` | Connect 中转商列表组件,展示已验证的中转服务商 |
| `VertexAISection.tsx` | Vertex AI 配置区域组件 |
| `AmpConfigSection.tsx` | Amp CLI 配置区域组件 |
| `GeminiApiKeySection.tsx` | Gemini API Key 配置区域组件 |
| `CodexSection.tsx` | Codex 配置区域组件 |
| `IFlowSection.tsx` | iFlow 配置区域组件 |
| `OAuthPluginTab.tsx` | OAuth 插件标签页组件 |
| `index.ts` | 组件导出入口 |
## 子目录
@@ -50,10 +52,12 @@ function App() {
- Requirements 1.1: API Key Provider 左右分栏布局
- Requirements 2.1, 2.2, 2.3: OAuth 凭证保持卡片布局
- Requirements 3.1-3.6: 完整支持 System Provider 类型
- Connect: 中转商浏览和一键添加功能
## 架构说明
ProviderPoolPage 支持三种分类:
ProviderPoolPage 支持四种分类:
1. **OAuth 凭证** - 使用卡片式布局显示 OAuth 类型凭证
2. **API Key** - 使用左右分栏布局(ApiKeyProviderSection)
3. **其他配置** - Vertex AI 和 Amp CLI 配置
3. **OAuth 插件** - 第三方 OAuth 插件管理
4. **Connect** - 中转商列表,支持浏览和一键获取 API Key
@@ -0,0 +1,257 @@
/**
* @file RelayProvidersSection 组件
* @description 中转商列表展示组件,支持浏览和一键跳转获取 API Key
* @module components/provider-pool/RelayProvidersSection
*
* _Requirements: Connect 中转商浏览功能_
*/
import { useState } from "react";
import {
RefreshCw,
ExternalLink,
Globe,
Mail,
MessageCircle,
Shield,
ShieldCheck,
Zap,
Clock,
AlertCircle,
} from "lucide-react";
import { useRelayRegistry } from "@/hooks/useRelayRegistry";
import type { RelayInfo } from "@/hooks/useDeepLink";
import { open } from "@tauri-apps/plugin-shell";
/**
* 中转商卡片组件
*/
function RelayProviderCard({ provider }: { provider: RelayInfo }) {
const [imageError, setImageError] = useState(false);
// 打开外部链接
const handleOpenLink = async (url: string) => {
try {
await open(url);
} catch (e) {
console.error("打开链接失败:", e);
// 回退到 window.open
window.open(url, "_blank");
}
};
// 获取 API Key 的链接(优先使用 dashboard,其次 website)
const getApiKeyLink = () => {
return provider.links.dashboard || provider.links.website;
};
return (
<div className="rounded-lg border bg-card p-4 hover:border-primary/50 transition-colors">
{/* 头部:Logo + 名称 */}
<div className="flex items-start gap-3 mb-3">
{/* Logo */}
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{provider.branding.logo && !imageError ? (
<img
src={provider.branding.logo}
alt={provider.name}
className="w-full h-full object-contain"
onError={() => setImageError(true)}
/>
) : (
<Globe className="w-6 h-6 text-muted-foreground" />
)}
</div>
{/* 名称和描述 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-foreground truncate">
{provider.name}
</h3>
{provider.features.verified && (
<ShieldCheck className="w-4 h-4 text-green-500 flex-shrink-0" />
)}
</div>
<p className="text-sm text-muted-foreground line-clamp-2 mt-0.5">
{provider.description}
</p>
</div>
</div>
{/* API 信息 */}
<div className="flex flex-wrap gap-2 mb-3">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-primary text-xs">
<Zap className="w-3 h-3" />
{provider.api.protocol.toUpperCase()}
</span>
{provider.features.streaming && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-500 text-xs">
流式响应
</span>
)}
{provider.features.models && provider.features.models.length > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-xs">
{provider.features.models.length} 个模型
</span>
)}
</div>
{/* 功能特性 */}
{provider.features.models && provider.features.models.length > 0 && (
<div className="mb-3">
<p className="text-xs text-muted-foreground mb-1">支持模型:</p>
<div className="flex flex-wrap gap-1">
{provider.features.models.slice(0, 5).map((model) => (
<span
key={model}
className="px-1.5 py-0.5 rounded bg-muted text-xs text-muted-foreground"
>
{model}
</span>
))}
{provider.features.models.length > 5 && (
<span className="px-1.5 py-0.5 rounded bg-muted text-xs text-muted-foreground">
+{provider.features.models.length - 5}
</span>
)}
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex items-center gap-2 pt-3 border-t">
{getApiKeyLink() && (
<button
onClick={() => handleOpenLink(getApiKeyLink()!)}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors"
>
<ExternalLink className="w-4 h-4" />
获取 API Key
</button>
)}
{provider.links.docs && (
<button
onClick={() => handleOpenLink(provider.links.docs!)}
className="flex items-center justify-center gap-1 px-3 py-2 rounded-lg border text-sm hover:bg-muted transition-colors"
title="查看文档"
>
<Globe className="w-4 h-4" />
</button>
)}
{provider.contact.email && (
<button
onClick={() => handleOpenLink(`mailto:${provider.contact.email}`)}
className="flex items-center justify-center gap-1 px-3 py-2 rounded-lg border text-sm hover:bg-muted transition-colors"
title="联系邮箱"
>
<Mail className="w-4 h-4" />
</button>
)}
{provider.contact.discord && (
<button
onClick={() => handleOpenLink(provider.contact.discord!)}
className="flex items-center justify-center gap-1 px-3 py-2 rounded-lg border text-sm hover:bg-muted transition-colors"
title="Discord"
>
<MessageCircle className="w-4 h-4" />
</button>
)}
</div>
</div>
);
}
/**
* 中转商列表组件
*/
export function RelayProvidersSection() {
const { providers, isLoading, error, refresh } = useRelayRegistry();
return (
<div className="space-y-4">
{/* 头部说明 */}
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-primary" />
<h3 className="font-semibold">ProxyCast Connect</h3>
</div>
<p className="text-sm text-muted-foreground mt-1">
浏览已验证的 AI API 中转服务商,获取 API Key
后可通过链接一键添加到凭证池
</p>
</div>
<button
onClick={refresh}
disabled={isLoading}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm hover:bg-muted disabled:opacity-50 transition-colors"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? "animate-spin" : ""}`} />
刷新
</button>
</div>
{/* 错误提示 */}
{error && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 text-yellow-600 dark:text-yellow-400">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
<span className="text-sm">{error.message}</span>
<button
onClick={refresh}
className="ml-auto text-sm underline hover:no-underline"
>
重试
</button>
</div>
)}
{/* 加载状态 */}
{isLoading && providers.length === 0 && (
<div className="flex items-center justify-center py-12">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
)}
{/* 空状态 */}
{!isLoading && providers.length === 0 && !error && (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Clock className="w-12 h-12 mb-4 opacity-50" />
<p className="text-lg">暂无中转商</p>
<p className="text-sm mt-1">点击刷新按钮加载中转商列表</p>
<button
onClick={refresh}
className="mt-4 flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90"
>
<RefreshCw className="w-4 h-4" />
加载中转商
</button>
</div>
)}
{/* 中转商列表 */}
{providers.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{providers.map((provider) => (
<RelayProviderCard key={provider.id} provider={provider} />
))}
</div>
)}
{/* 底部说明 */}
{providers.length > 0 && (
<div className="text-center text-xs text-muted-foreground pt-4 border-t">
<p>
获取 API Key 后,中转商会提供一个 <code>proxycast://</code>{" "}
链接,点击即可一键添加到凭证池
</p>
</div>
)}
</div>
);
}
export default RelayProvidersSection;
+1
View File
@@ -1,6 +1,7 @@
export { useFlowEvents } from "./useFlowEvents";
export { useOAuthPlugins, useSingleOAuthPlugin } from "./useOAuthPlugins";
export { useDeepLink } from "./useDeepLink";
export { useModelRegistry } from "./useModelRegistry";
export type {
ConnectPayload,
RelayInfo,
+6
View File
@@ -54,6 +54,10 @@ export interface RelayLinks {
docs?: string;
/** 状态页链接 */
status?: string;
/** 控制台/仪表盘链接 */
dashboard?: string;
/** 网站链接 */
website?: string;
}
/**
@@ -92,6 +96,8 @@ export interface RelayFeatures {
function_calling: boolean;
/** 是否支持视觉模型 */
vision: boolean;
/** 是否已验证 */
verified?: boolean;
}
/**
+333
View File
@@ -0,0 +1,333 @@
/**
* 模型注册表 Hook
*
* 提供模型数据管理、搜索、收藏等功能
*/
import { useState, useEffect, useCallback, useMemo } from "react";
import { modelRegistryApi } from "@/lib/api/modelRegistry";
import type {
EnhancedModelMetadata,
UserModelPreference,
ModelTier,
} from "@/lib/types/modelRegistry";
interface UseModelRegistryOptions {
/** 自动加载 */
autoLoad?: boolean;
/** 过滤的 Provider ID 列表 */
providerFilter?: string[];
/** 过滤的服务等级 */
tierFilter?: ModelTier[];
/** 只显示收藏 */
favoritesOnly?: boolean;
}
interface UseModelRegistryReturn {
/** 模型列表 */
models: EnhancedModelMetadata[];
/** 用户偏好 */
preferences: Map<string, UserModelPreference>;
/** 是否加载中 */
loading: boolean;
/** 错误信息 */
error: string | null;
/** 最后同步时间 */
lastSyncAt: number | null;
/** 刷新模型列表 */
refresh: () => Promise<void>;
/** 搜索模型 */
search: (query: string) => EnhancedModelMetadata[];
/** 切换收藏 */
toggleFavorite: (modelId: string) => Promise<void>;
/** 隐藏模型 */
hideModel: (modelId: string) => Promise<void>;
/** 获取模型详情 */
getModel: (modelId: string) => EnhancedModelMetadata | undefined;
/** 按 Provider 分组 */
groupedByProvider: Map<string, EnhancedModelMetadata[]>;
/** 按等级分组 */
groupedByTier: Map<ModelTier, EnhancedModelMetadata[]>;
}
/**
* 智能排序函数
*/
function sortModels(
models: EnhancedModelMetadata[],
preferences: Map<string, UserModelPreference>
): EnhancedModelMetadata[] {
return [...models].sort((a, b) => {
const prefA = preferences.get(a.id);
const prefB = preferences.get(b.id);
// 1. 收藏优先
if (prefA?.is_favorite && !prefB?.is_favorite) return -1;
if (!prefA?.is_favorite && prefB?.is_favorite) return 1;
// 2. 最新版本优先
if (a.is_latest && !b.is_latest) return -1;
if (!a.is_latest && b.is_latest) return 1;
// 3. 活跃状态优先
if (a.status === "active" && b.status !== "active") return -1;
if (a.status !== "active" && b.status === "active") return 1;
// 4. 使用频率
const usageA = prefA?.usage_count || 0;
const usageB = prefB?.usage_count || 0;
if (usageA !== usageB) return usageB - usageA;
// 5. 按名称字母序
return a.display_name.localeCompare(b.display_name);
});
}
/**
* 简单的模糊搜索
*/
function fuzzySearch(
models: EnhancedModelMetadata[],
query: string
): EnhancedModelMetadata[] {
if (!query.trim()) {
return models;
}
const queryLower = query.toLowerCase();
return models
.map((model) => {
let score = 0;
// 精确匹配 ID
if (model.id.toLowerCase() === queryLower) {
score += 100;
} else if (model.id.toLowerCase().includes(queryLower)) {
score += 50;
}
// 显示名称匹配
if (model.display_name.toLowerCase().includes(queryLower)) {
score += 30;
}
// Provider 匹配
if (model.provider_name.toLowerCase().includes(queryLower)) {
score += 20;
}
// 家族匹配
if (model.family?.toLowerCase().includes(queryLower)) {
score += 15;
}
// 最新版本加分
if (model.is_latest) {
score += 5;
}
// 活跃状态加分
if (model.status === "active") {
score += 3;
}
return { model, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.map(({ model }) => model);
}
export function useModelRegistry(
options: UseModelRegistryOptions = {}
): UseModelRegistryReturn {
const {
autoLoad = true,
providerFilter,
tierFilter,
favoritesOnly = false,
} = options;
const [allModels, setAllModels] = useState<EnhancedModelMetadata[]>([]);
const [preferences, setPreferences] = useState<
Map<string, UserModelPreference>
>(new Map());
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
// 加载模型数据
const loadModels = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [models, prefs, syncState] = await Promise.all([
modelRegistryApi.getModelRegistry(),
modelRegistryApi.getModelPreferences(),
modelRegistryApi.getModelSyncState(),
]);
setAllModels(models);
setPreferences(new Map(prefs.map((p) => [p.model_id, p])));
setLastSyncAt(syncState.last_sync_at);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
}, []);
// 刷新(强制从 models.dev 获取)
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
await modelRegistryApi.refreshModelRegistry();
await loadModels();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
}, [loadModels]);
// 过滤后的模型列表
const models = useMemo(() => {
let filtered = allModels;
// Provider 过滤
if (providerFilter && providerFilter.length > 0) {
filtered = filtered.filter((m) => providerFilter.includes(m.provider_id));
}
// 等级过滤
if (tierFilter && tierFilter.length > 0) {
filtered = filtered.filter((m) =>
tierFilter.includes(m.tier as ModelTier)
);
}
// 收藏过滤
if (favoritesOnly) {
filtered = filtered.filter((m) => preferences.get(m.id)?.is_favorite);
}
// 隐藏过滤
filtered = filtered.filter((m) => !preferences.get(m.id)?.is_hidden);
// 智能排序
return sortModels(filtered, preferences);
}, [allModels, providerFilter, tierFilter, favoritesOnly, preferences]);
// 模糊搜索
const search = useCallback(
(query: string): EnhancedModelMetadata[] => {
return fuzzySearch(models, query);
},
[models]
);
// 切换收藏
const toggleFavorite = useCallback(async (modelId: string) => {
try {
const newState = await modelRegistryApi.toggleModelFavorite(modelId);
setPreferences((prev) => {
const newPrefs = new Map(prev);
const current = newPrefs.get(modelId);
if (current) {
newPrefs.set(modelId, {
...current,
is_favorite: newState,
});
} else {
newPrefs.set(modelId, {
model_id: modelId,
is_favorite: newState,
is_hidden: false,
custom_alias: null,
usage_count: 0,
last_used_at: null,
created_at: Date.now() / 1000,
updated_at: Date.now() / 1000,
});
}
return newPrefs;
});
} catch (e) {
console.error("Failed to toggle favorite:", e);
}
}, []);
// 隐藏模型
const hideModel = useCallback(async (modelId: string) => {
try {
await modelRegistryApi.hideModel(modelId);
setPreferences((prev) => {
const newPrefs = new Map(prev);
const current = newPrefs.get(modelId);
if (current) {
newPrefs.set(modelId, { ...current, is_hidden: true });
}
return newPrefs;
});
} catch (e) {
console.error("Failed to hide model:", e);
}
}, []);
// 获取单个模型
const getModel = useCallback(
(modelId: string) => {
return allModels.find((m) => m.id === modelId);
},
[allModels]
);
// 按 Provider 分组
const groupedByProvider = useMemo(() => {
const groups = new Map<string, EnhancedModelMetadata[]>();
for (const model of models) {
const existing = groups.get(model.provider_id) || [];
existing.push(model);
groups.set(model.provider_id, existing);
}
return groups;
}, [models]);
// 按等级分组
const groupedByTier = useMemo(() => {
const groups = new Map<ModelTier, EnhancedModelMetadata[]>();
for (const model of models) {
const tier = model.tier as ModelTier;
const existing = groups.get(tier) || [];
existing.push(model);
groups.set(tier, existing);
}
return groups;
}, [models]);
// 自动加载
useEffect(() => {
if (autoLoad) {
loadModels();
}
}, [autoLoad, loadModels]);
return {
models,
preferences,
loading,
error,
lastSyncAt,
refresh,
search,
toggleFavorite,
hideModel,
getModel,
groupedByProvider,
groupedByTier,
};
}
+114
View File
@@ -0,0 +1,114 @@
/**
* 模型注册表 API
*
* 提供与后端 ModelRegistryService 交互的 API
*/
import { invoke } from "@tauri-apps/api/core";
import type {
EnhancedModelMetadata,
ModelSyncState,
ModelTier,
UserModelPreference,
} from "@/lib/types/modelRegistry";
/**
* 获取所有模型
*/
export async function getModelRegistry(): Promise<EnhancedModelMetadata[]> {
return invoke("get_model_registry");
}
/**
* 刷新模型注册表(从 models.dev 获取最新数据)
*/
export async function refreshModelRegistry(): Promise<void> {
return invoke("refresh_model_registry");
}
/**
* 搜索模型
* @param query 搜索关键词
* @param limit 返回数量限制
*/
export async function searchModels(
query: string,
limit?: number
): Promise<EnhancedModelMetadata[]> {
return invoke("search_models", { query, limit });
}
/**
* 获取用户模型偏好
*/
export async function getModelPreferences(): Promise<UserModelPreference[]> {
return invoke("get_model_preferences");
}
/**
* 切换模型收藏状态
* @param modelId 模型 ID
* @returns 新的收藏状态
*/
export async function toggleModelFavorite(modelId: string): Promise<boolean> {
return invoke("toggle_model_favorite", { modelId });
}
/**
* 隐藏模型
* @param modelId 模型 ID
*/
export async function hideModel(modelId: string): Promise<void> {
return invoke("hide_model", { modelId });
}
/**
* 记录模型使用
* @param modelId 模型 ID
*/
export async function recordModelUsage(modelId: string): Promise<void> {
return invoke("record_model_usage", { modelId });
}
/**
* 获取模型同步状态
*/
export async function getModelSyncState(): Promise<ModelSyncState> {
return invoke("get_model_sync_state");
}
/**
* 按 Provider 获取模型
* @param providerId Provider ID
*/
export async function getModelsForProvider(
providerId: string
): Promise<EnhancedModelMetadata[]> {
return invoke("get_models_for_provider", { providerId });
}
/**
* 按服务等级获取模型
* @param tier 服务等级
*/
export async function getModelsByTier(
tier: ModelTier
): Promise<EnhancedModelMetadata[]> {
return invoke("get_models_by_tier", { tier });
}
/**
* 模型注册表 API 对象
*/
export const modelRegistryApi = {
getModelRegistry,
refreshModelRegistry,
searchModels,
getModelPreferences,
toggleModelFavorite,
hideModel,
recordModelUsage,
getModelSyncState,
getModelsForProvider,
getModelsByTier,
};
+26 -16
View File
@@ -27,23 +27,29 @@ export type TaskHint =
/** 可用模型 */
export interface AvailableModel {
/** 模型 ID */
model_id: string;
id: string;
/** 显示名称 */
display_name: string;
/** Provider 类型 */
provider_type: string;
/** 模型家族 */
family?: string;
/** 凭证 ID */
credential_id: string;
/** 上下文长度 */
context_length?: number;
/** 是否支持视觉 */
supports_vision: boolean;
/** 是否支持工具 */
supports_tools: boolean;
/** 输入价格(每 1M tokens) */
input_cost_per_million?: number;
/** 输出价格(每 1M tokens) */
output_cost_per_million?: number;
/** 是否健康 */
is_healthy: boolean;
/** 当前负载 (0-100) */
current_load?: number;
/** 上下文长度 */
context_length?: number;
/** 是否支持视觉 */
supports_vision?: boolean;
/** 是否支持工具 */
supports_tools?: boolean;
}
/** 模型池统计 */
@@ -62,16 +68,20 @@ export interface PoolStats {
/** 选择结果 */
export interface SelectionResult {
/** 选中的模型 ID */
model_id: string;
/** Provider 类型 */
provider_type: string;
/** 凭证 ID */
credential_id: string;
/** 使用的策略 */
strategy_used: string;
/** 选中的模型 */
model: AvailableModel;
/** 使用的策略 ID */
strategy_id: string;
/** 选择原因 */
reason?: string;
reason: string;
/** 置信度 (0-100) */
confidence: number;
/** 服务等级 */
tier: ServiceTier;
/** 是否是降级选择 */
is_fallback: boolean;
/** 降级原因(如果是降级) */
fallback_reason?: string;
}
/** 选择请求 */
@@ -117,6 +117,24 @@ export function PluginUIRenderer({
// 错误
if (error) {
// 检查是否是文件不存在的错误
const isFileNotFound = error.includes("读取插件 UI 文件失败") ||
error.includes("No such file") ||
error.includes("not found") ||
error.includes("没有找到有效的组件导出") ||
error.includes("插件加载失败");
if (isFileNotFound) {
// UI 文件不存在时显示友好提示
return fallback ? <>{fallback}</> : (
<div className={`flex flex-col items-center justify-center p-8 text-muted-foreground ${className}`}>
<AlertCircle className="h-8 w-8 mb-2 opacity-50" />
<p className="text-center text-sm">该插件暂无 UI 界面</p>
<p className="text-center text-xs mt-1 opacity-70">请通过命令行或 API 使用此插件</p>
</div>
);
}
return (
<div
className={`flex flex-col items-center justify-center p-8 ${className}`}
+49 -4
View File
@@ -31,6 +31,47 @@ export interface PluginModule {
*/
const loadedPlugins = new Map<string, PluginModule>();
/**
* 插件 ID 到全局变量名的映射
* 格式: pluginId -> GlobalVariableName
*/
const PLUGIN_GLOBAL_NAMES: Record<string, string> = {
"kiro-provider": "KiroProviderPlugin",
"droid-provider": "DroidProviderPlugin",
"claude-provider": "ClaudeProviderPlugin",
"gemini-provider": "GeminiProviderPlugin",
"antigravity-provider": "AntigravityProviderPlugin",
};
/**
* 根据插件 ID 获取全局变量名
* 如果没有预定义,则尝试从路径推断
*/
function getPluginGlobalName(pluginPath: string): string {
// 从路径中提取插件 ID
const parts = pluginPath.split("/");
const pluginIdIndex = parts.findIndex((p) => p.endsWith("-provider"));
const pluginId = pluginIdIndex >= 0 ? parts[pluginIdIndex] : null;
// 查找预定义的全局变量名
if (pluginId && PLUGIN_GLOBAL_NAMES[pluginId]) {
return PLUGIN_GLOBAL_NAMES[pluginId];
}
// 尝试从插件 ID 推断全局变量名
// 例如: my-plugin -> MyPluginPlugin
if (pluginId) {
const camelCase = pluginId
.split("-")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("");
return camelCase;
}
// 默认回退
return "KiroProviderPlugin";
}
/**
* 读取插件文件内容
*/
@@ -92,22 +133,26 @@ export async function loadPluginUI(
// 读取插件文件内容
const content = await readPluginFile(pluginPath);
// 获取插件的全局变量名
const globalName = getPluginGlobalName(pluginPath);
console.log(`[PluginLoader] 加载插件: ${pluginPath}`);
console.log(`[PluginLoader] 全局变量名: ${globalName}`);
console.log(
`[PluginLoader] 全局变量检查: React=${typeof (window as unknown as Record<string, unknown>).React}, ProxyCastPluginComponents=${typeof (window as unknown as Record<string, unknown>).ProxyCastPluginComponents}`,
);
// 执行插件代码
// IIFE 格式会自动将结果赋值给 window.KiroProviderPlugin
await executeScript(content);
// 获取插件模块
const pluginExports = (window as unknown as Record<string, unknown>)
.KiroProviderPlugin as Record<string, unknown> | undefined;
const pluginExports = (window as unknown as Record<string, unknown>)[
globalName
] as Record<string, unknown> | undefined;
if (!pluginExports) {
console.error(
`[PluginLoader] 插件 ${pluginPath} 没有导出到 window.KiroProviderPlugin`,
`[PluginLoader] 插件 ${pluginPath} 没有导出到 window.${globalName}`,
);
return null;
}
+144
View File
@@ -0,0 +1,144 @@
/**
* 模型注册表类型定义
*
* 借鉴 opencode 的模型管理方式,定义增强的模型元数据结构
*/
/** 模型能力 */
export interface ModelCapabilities {
/** 是否支持视觉输入 */
vision: boolean;
/** 是否支持工具调用 */
tools: boolean;
/** 是否支持流式输出 */
streaming: boolean;
/** 是否支持 JSON 模式 */
json_mode: boolean;
/** 是否支持函数调用 */
function_calling: boolean;
/** 是否支持推理/思考 */
reasoning: boolean;
}
/** 模型定价 */
export interface ModelPricing {
/** 输入价格(每百万 token) */
input_per_million: number | null;
/** 输出价格(每百万 token) */
output_per_million: number | null;
/** 缓存读取价格(每百万 token) */
cache_read_per_million: number | null;
/** 缓存写入价格(每百万 token) */
cache_write_per_million: number | null;
/** 货币单位 */
currency: string;
}
/** 模型限制 */
export interface ModelLimits {
/** 上下文长度 */
context_length: number | null;
/** 最大输出 token 数 */
max_output_tokens: number | null;
/** 每分钟请求数限制 */
requests_per_minute: number | null;
/** 每分钟 token 数限制 */
tokens_per_minute: number | null;
}
/** 模型状态 */
export type ModelStatus =
| "active"
| "preview"
| "alpha"
| "beta"
| "deprecated"
| "legacy";
/** 模型服务等级 */
export type ModelTier = "mini" | "pro" | "max";
/** 模型数据来源 */
export type ModelSource = "models.dev" | "local" | "custom";
/** 增强的模型元数据 */
export interface EnhancedModelMetadata {
/** 模型 ID (如 "claude-sonnet-4-5-20250514") */
id: string;
/** 显示名称 (如 "Claude Sonnet 4.5") */
display_name: string;
/** Provider ID (如 "anthropic", "openai", "dashscope") */
provider_id: string;
/** Provider 显示名称 */
provider_name: string;
/** 模型家族 (如 "sonnet", "gpt-4", "qwen") */
family: string | null;
/** 服务等级 */
tier: ModelTier;
/** 模型能力 */
capabilities: ModelCapabilities;
/** 定价信息 */
pricing: ModelPricing | null;
/** 限制信息 */
limits: ModelLimits;
/** 模型状态 */
status: ModelStatus;
/** 发布日期 */
release_date: string | null;
/** 是否为最新版本 */
is_latest: boolean;
/** 描述 */
description: string | null;
/** 数据来源 */
source: ModelSource;
/** 创建时间 (Unix 时间戳) */
created_at: number;
/** 最后更新时间 (Unix 时间戳) */
updated_at: number;
}
/** 用户模型偏好 */
export interface UserModelPreference {
/** 模型 ID */
model_id: string;
/** 是否收藏 */
is_favorite: boolean;
/** 是否隐藏 */
is_hidden: boolean;
/** 自定义别名 */
custom_alias: string | null;
/** 使用次数 */
usage_count: number;
/** 最后使用时间 (Unix 时间戳) */
last_used_at: number | null;
/** 创建时间 (Unix 时间戳) */
created_at: number;
/** 更新时间 (Unix 时间戳) */
updated_at: number;
}
/** 模型同步状态 */
export interface ModelSyncState {
/** 最后同步时间 (Unix 时间戳) */
last_sync_at: number | null;
/** 同步的模型数量 */
model_count: number;
/** 是否正在同步 */
is_syncing: boolean;
/** 最后同步错误 */
last_error: string | null;
}
/** 模型注册表状态 */
export interface ModelRegistryState {
/** 模型列表 */
models: EnhancedModelMetadata[];
/** 用户偏好 */
preferences: Map<string, UserModelPreference>;
/** 最后同步时间 */
lastSyncAt: number | null;
/** 是否加载中 */
isLoading: boolean;
/** 错误信息 */
error: string | null;
}