feat: implement Pi Switch i18n UI and agent config switch
This commit is contained in:
+227
-50
@@ -1,66 +1,243 @@
|
||||
use crate::models::{Config, ProfileSummary};
|
||||
use crate::models::{Agent, Config, DiscoveredModel, Provider, ProviderType};
|
||||
use crate::pi_io;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
fn fmt_anyhow(err: anyhow::Error) -> String {
|
||||
err.chain()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(": ")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_config_command() -> Config {
|
||||
Config {
|
||||
default_model: None,
|
||||
providers: indexmap::IndexMap::new(),
|
||||
models: indexmap::IndexMap::new(),
|
||||
raw_other: toml_edit::Table::new(),
|
||||
pub fn debug_log(message: String) {
|
||||
eprintln!("[frontend] {}", message);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
||||
match agent {
|
||||
Agent::KimiCode => crate::kimi_code_io::load_kimi_code_config_as_config()
|
||||
.map_err(fmt_anyhow),
|
||||
Agent::Pi => {
|
||||
let file = pi_io::load_pi_models().map_err(fmt_anyhow)?;
|
||||
Ok(pi_io::pi_file_to_config(&file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_config_command(_config: Config) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_profiles() -> Vec<ProfileSummary> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_profile(_filename: String) -> Config {
|
||||
Config {
|
||||
default_model: None,
|
||||
providers: indexmap::IndexMap::new(),
|
||||
models: indexmap::IndexMap::new(),
|
||||
raw_other: toml_edit::Table::new(),
|
||||
pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), String> {
|
||||
match agent {
|
||||
Agent::KimiCode => crate::kimi_code_io::save_config_as_kimi_code(&config).map_err(fmt_anyhow),
|
||||
Agent::Pi => {
|
||||
let file = pi_io::config_to_pi_file(&config);
|
||||
pi_io::save_pi_models(&file).map_err(fmt_anyhow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_profile(_filename: String, _config: Config) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn switch_profile(_filename: String) -> Result<Config, String> {
|
||||
Ok(Config {
|
||||
default_model: None,
|
||||
providers: indexmap::IndexMap::new(),
|
||||
models: indexmap::IndexMap::new(),
|
||||
raw_other: toml_edit::Table::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn rename_profile(_old_filename: String, _new_name: String) -> Result<String, String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_profile(_filename: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_config_dir() -> Result<(), String> {
|
||||
Ok(())
|
||||
pub fn open_agent_config_dir(app: tauri::AppHandle, agent: Agent) -> Result<(), String> {
|
||||
let path = agent.config_dir();
|
||||
std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
app.opener()
|
||||
.open_path(&path_str, None::<&str>)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Fetch available models from a provider's API.
|
||||
#[tauri::command]
|
||||
pub async fn list_provider_models(provider: Provider) -> Result<Vec<DiscoveredModel>, String> {
|
||||
let api_key = resolve_api_key(&provider)
|
||||
.ok_or_else(|| format!("Provider '{}' has no API key configured", provider.name))?;
|
||||
|
||||
let base = resolve_base_url(&provider);
|
||||
|
||||
match provider.provider_type {
|
||||
ProviderType::Kimi | ProviderType::Openai | ProviderType::OpenaiResponses => {
|
||||
fetch_openai_models(&base, &api_key).await
|
||||
}
|
||||
ProviderType::Anthropic => fetch_anthropic_models(&base, &api_key).await,
|
||||
ProviderType::GoogleGenai => fetch_google_genai_models(&base, &api_key).await,
|
||||
ProviderType::Vertexai => Err("Vertex AI model discovery requires GCP project/location configuration and is not yet supported".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_api_key(provider: &Provider) -> Option<String> {
|
||||
if provider.managed {
|
||||
return Some("managed".to_string());
|
||||
}
|
||||
if let Some(key) = &provider.api_key {
|
||||
if !key.is_empty() {
|
||||
return Some(key.clone());
|
||||
}
|
||||
}
|
||||
let env_key = expected_api_key_key(&provider.provider_type);
|
||||
provider.env.get(env_key).filter(|s| !s.is_empty()).cloned()
|
||||
}
|
||||
|
||||
fn resolve_base_url(provider: &Provider) -> String {
|
||||
provider
|
||||
.base_url
|
||||
.clone()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
provider
|
||||
.provider_type
|
||||
.default_base_url()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn expected_api_key_key(provider_type: &ProviderType) -> &'static str {
|
||||
match provider_type {
|
||||
ProviderType::Kimi => "KIMI_API_KEY",
|
||||
ProviderType::Anthropic => "ANTHROPIC_API_KEY",
|
||||
ProviderType::Openai | ProviderType::OpenaiResponses => "OPENAI_API_KEY",
|
||||
ProviderType::GoogleGenai => "GOOGLE_API_KEY",
|
||||
ProviderType::Vertexai => "VERTEXAI_API_KEY",
|
||||
}
|
||||
}
|
||||
|
||||
// ── OpenAI-compatible /models endpoint ──────────────────────────────
|
||||
|
||||
async fn fetch_openai_models(base: &str, api_key: &str) -> Result<Vec<DiscoveredModel>, String> {
|
||||
let url = format!("{}/models", base.trim_end_matches('/'));
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.bearer_auth(api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request to {} failed: {e}", url))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("{} returned HTTP {}", url, resp.status()));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OaiModel {
|
||||
id: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OaiList {
|
||||
data: Vec<OaiModel>,
|
||||
}
|
||||
|
||||
let body: OaiList = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("failed to parse response from {}: {e}", url))?;
|
||||
|
||||
Ok(body
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|m| DiscoveredModel {
|
||||
id: m.id,
|
||||
display_name: None,
|
||||
max_context_size: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// ── Anthropic /v1/models endpoint ───────────────────────────────────
|
||||
|
||||
async fn fetch_anthropic_models(base: &str, api_key: &str) -> Result<Vec<DiscoveredModel>, String> {
|
||||
let url = format!("{}/v1/models", base.trim_end_matches('/'));
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request to {} failed: {e}", url))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("{} returned HTTP {}", url, resp.status()));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AntModel {
|
||||
id: String,
|
||||
display_name: Option<String>,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AntList {
|
||||
data: Vec<AntModel>,
|
||||
}
|
||||
|
||||
let body: AntList = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("failed to parse response from {}: {e}", url))?;
|
||||
|
||||
Ok(body
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|m| DiscoveredModel {
|
||||
id: m.id,
|
||||
display_name: m.display_name,
|
||||
max_context_size: None,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// ── Google GenAI /v1beta/models endpoint ────────────────────────────
|
||||
|
||||
async fn fetch_google_genai_models(
|
||||
base: &str,
|
||||
api_key: &str,
|
||||
) -> Result<Vec<DiscoveredModel>, String> {
|
||||
let url = format!(
|
||||
"{}/v1beta/models?key={}",
|
||||
base.trim_end_matches('/'),
|
||||
api_key
|
||||
);
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request to {} failed: {e}", url))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("{} returned HTTP {}", url, resp.status()));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GglModel {
|
||||
name: String,
|
||||
#[serde(rename = "displayName")]
|
||||
display_name: Option<String>,
|
||||
#[serde(rename = "outputTokenLimit")]
|
||||
output_token_limit: Option<u64>,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GglList {
|
||||
models: Vec<GglModel>,
|
||||
}
|
||||
|
||||
let body: GglList = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("failed to parse response from {}: {e}", url))?;
|
||||
|
||||
Ok(body
|
||||
.models
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let id = m.name.strip_prefix("models/").unwrap_or(&m.name).to_string();
|
||||
DiscoveredModel {
|
||||
id,
|
||||
display_name: m.display_name,
|
||||
max_context_size: m.output_token_limit,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user