Compare commits
5 Commits
baaefa2909
...
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
c9073ff4a7
|
|||
|
88324c21c2
|
|||
|
ffc9741d1e
|
|||
| 5638315031 | |||
| 2e43a5e396 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,4 +1,11 @@
|
||||
## v1.0.1 更新日志
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.4] - 2026-02-01
|
||||
|
||||
### ✨ 新功能
|
||||
- 新增 `test3.txt`,支持中文输出测试
|
||||
@@ -19,4 +26,21 @@
|
||||
|
||||
### 🔧 其他变更
|
||||
- 新增个人访问令牌、使用统计与配置校验功能
|
||||
- 添加 `test2.txt` 占位文件
|
||||
- 添加 `test2.txt` 占位文件
|
||||
|
||||
## [0.1.0] - 2026-01-30
|
||||
|
||||
### Added
|
||||
- Initial project structure
|
||||
- Core functionality for git operations
|
||||
- LLM integration
|
||||
- Configuration management
|
||||
- CLI interface
|
||||
|
||||
### Features
|
||||
- **Commit Generation**: Automatically generate conventional commit messages from git diffs
|
||||
- **Profile Management**: Switch between multiple Git identities for different contexts
|
||||
- **Tag Management**: Create annotated tags with AI-generated release notes
|
||||
- **Changelog**: Generate and maintain changelog in Keep a Changelog format
|
||||
- **Security**: Encrypt SSH passphrases and API keys
|
||||
- **Interactive UI**: Beautiful CLI with prompts and previews
|
||||
@@ -350,8 +350,23 @@ impl CommitCommand {
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to amend commit: {}", stderr);
|
||||
|
||||
let error_msg = if stderr.is_empty() {
|
||||
if stdout.is_empty() {
|
||||
"GPG signing failed. Please check:\n\
|
||||
1. GPG signing key is configured (git config --get user.signingkey)\n\
|
||||
2. GPG agent is running\n\
|
||||
3. You can sign commits manually (try: git commit --amend -S)".to_string()
|
||||
} else {
|
||||
stdout.to_string()
|
||||
}
|
||||
} else {
|
||||
stderr.to_string()
|
||||
};
|
||||
|
||||
bail!("Failed to amend commit: {}", error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -830,14 +830,14 @@ impl ConfigCommand {
|
||||
async fn list_models(&self) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
let config = manager.config();
|
||||
|
||||
|
||||
match config.llm.provider.as_str() {
|
||||
"ollama" => {
|
||||
let client = crate::llm::OllamaClient::new(
|
||||
&config.llm.ollama.url,
|
||||
&config.llm.ollama.model,
|
||||
);
|
||||
|
||||
|
||||
println!("Fetching available models from Ollama...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
@@ -859,7 +859,7 @@ impl ConfigCommand {
|
||||
key,
|
||||
&config.llm.openai.model,
|
||||
)?;
|
||||
|
||||
|
||||
println!("Fetching available models from OpenAI...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
@@ -877,11 +877,110 @@ impl ConfigCommand {
|
||||
bail!("OpenAI API key not configured");
|
||||
}
|
||||
}
|
||||
"anthropic" => {
|
||||
if let Some(ref key) = config.llm.anthropic.api_key {
|
||||
let client = crate::llm::AnthropicClient::new(
|
||||
key,
|
||||
&config.llm.anthropic.model,
|
||||
)?;
|
||||
|
||||
println!("Fetching available models from Anthropic...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
println!("\n{}", "Available models:".bold());
|
||||
for model in models {
|
||||
let marker = if model == config.llm.anthropic.model { "●".green() } else { "○".dimmed() };
|
||||
println!("{} {}", marker, model);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("Anthropic API key not configured");
|
||||
}
|
||||
}
|
||||
"kimi" => {
|
||||
if let Some(ref key) = config.llm.kimi.api_key {
|
||||
let client = crate::llm::KimiClient::with_base_url(
|
||||
key,
|
||||
&config.llm.kimi.model,
|
||||
&config.llm.kimi.base_url,
|
||||
)?;
|
||||
|
||||
println!("Fetching available models from Kimi...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
println!("\n{}", "Available models:".bold());
|
||||
for model in models {
|
||||
let marker = if model == config.llm.kimi.model { "●".green() } else { "○".dimmed() };
|
||||
println!("{} {}", marker, model);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("Kimi API key not configured");
|
||||
}
|
||||
}
|
||||
"deepseek" => {
|
||||
if let Some(ref key) = config.llm.deepseek.api_key {
|
||||
let client = crate::llm::DeepSeekClient::with_base_url(
|
||||
key,
|
||||
&config.llm.deepseek.model,
|
||||
&config.llm.deepseek.base_url,
|
||||
)?;
|
||||
|
||||
println!("Fetching available models from DeepSeek...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
println!("\n{}", "Available models:".bold());
|
||||
for model in models {
|
||||
let marker = if model == config.llm.deepseek.model { "●".green() } else { "○".dimmed() };
|
||||
println!("{} {}", marker, model);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("DeepSeek API key not configured");
|
||||
}
|
||||
}
|
||||
"openrouter" => {
|
||||
if let Some(ref key) = config.llm.openrouter.api_key {
|
||||
let client = crate::llm::OpenRouterClient::with_base_url(
|
||||
key,
|
||||
&config.llm.openrouter.model,
|
||||
&config.llm.openrouter.base_url,
|
||||
)?;
|
||||
|
||||
println!("Fetching available models from OpenRouter...");
|
||||
match client.list_models().await {
|
||||
Ok(models) => {
|
||||
println!("\n{}", "Available models:".bold());
|
||||
for model in models {
|
||||
let marker = if model == config.llm.openrouter.model { "●".green() } else { "○".dimmed() };
|
||||
println!("{} {}", marker, model);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("OpenRouter API key not configured");
|
||||
}
|
||||
}
|
||||
provider => {
|
||||
println!("Listing models not supported for provider: {}", provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -437,10 +437,18 @@ impl ProfileCommand {
|
||||
|
||||
let repo_path = repo.path().to_string_lossy().to_string();
|
||||
|
||||
manager.set_repo_profile(repo_path, name.to_string())?;
|
||||
manager.set_repo_profile(repo_path.clone(), name.to_string())?;
|
||||
|
||||
// Get the profile and apply it to the repository
|
||||
let profile = manager.get_profile(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?;
|
||||
|
||||
profile.apply_to_repo(repo.inner())?;
|
||||
manager.record_profile_usage(name, Some(repo_path))?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Set '{}' for current repository", "✓".green(), name.cyan());
|
||||
println!("{} Applied profile '{}' to current repository", "✓".green(), name.cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -194,6 +194,21 @@ impl GitProfile {
|
||||
|
||||
if let Some(key) = self.signing_key() {
|
||||
config.set_str("user.signingkey", key)?;
|
||||
|
||||
if self.settings.auto_sign_commits {
|
||||
config.set_bool("commit.gpgsign", true)?;
|
||||
}
|
||||
|
||||
if self.settings.auto_sign_tags {
|
||||
config.set_bool("tag.gpgsign", true)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref ssh) = self.ssh {
|
||||
if let Some(ref key_path) = ssh.private_key_path {
|
||||
config.set_str("core.sshCommand",
|
||||
&format!("ssh -i {}", key_path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::config::{CommitFormat, LlmConfig, Language};
|
||||
use crate::git::{CommitInfo, GitRepo};
|
||||
use crate::llm::{GeneratedCommit, LlmClient};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
|
||||
/// Content generator using LLM
|
||||
pub struct ContentGenerator {
|
||||
@@ -114,8 +113,7 @@ impl ContentGenerator {
|
||||
format: CommitFormat,
|
||||
language: Language,
|
||||
) -> Result<GeneratedCommit> {
|
||||
use dialoguer::{Confirm, Select};
|
||||
use console::Term;
|
||||
use dialoguer::Select;
|
||||
|
||||
let diff = repo.get_staged_diff()?;
|
||||
|
||||
@@ -145,7 +143,6 @@ impl ContentGenerator {
|
||||
"✓ Accept and commit",
|
||||
"🔄 Regenerate",
|
||||
"✏️ Edit",
|
||||
"📋 Copy to clipboard",
|
||||
"❌ Cancel",
|
||||
];
|
||||
|
||||
@@ -165,24 +162,13 @@ impl ContentGenerator {
|
||||
let edited = crate::utils::editor::edit_content(&generated.to_conventional())?;
|
||||
generated = self.parse_edited_commit(&edited, format)?;
|
||||
}
|
||||
3 => {
|
||||
#[cfg(feature = "clipboard")]
|
||||
{
|
||||
arboard::Clipboard::new()?.set_text(generated.to_conventional())?;
|
||||
println!("Copied to clipboard!");
|
||||
}
|
||||
#[cfg(not(feature = "clipboard"))]
|
||||
{
|
||||
println!("Clipboard feature not enabled");
|
||||
}
|
||||
}
|
||||
4 => anyhow::bail!("Cancelled by user"),
|
||||
3 => anyhow::bail!("Cancelled by user"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_edited_commit(&self, edited: &str, format: CommitFormat) -> Result<GeneratedCommit> {
|
||||
fn parse_edited_commit(&self, edited: &str, _format: CommitFormat) -> Result<GeneratedCommit> {
|
||||
let parsed = crate::git::commit::parse_commit_message(edited);
|
||||
|
||||
Ok(GeneratedCommit {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{CommitInfo, GitRepo};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -232,8 +232,6 @@ impl ChangelogGenerator {
|
||||
let mut breaking = vec![];
|
||||
|
||||
for commit in commits {
|
||||
let msg = commit.subject();
|
||||
|
||||
if commit.message.contains("BREAKING CHANGE") {
|
||||
breaking.push(commit);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::GitRepo;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::Local;
|
||||
|
||||
/// Commit builder for creating commits
|
||||
@@ -174,8 +174,23 @@ impl CommitBuilder {
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to amend commit: {}", stderr);
|
||||
|
||||
let error_msg = if stderr.is_empty() {
|
||||
if stdout.is_empty() {
|
||||
"GPG signing failed. Please check:\n\
|
||||
1. GPG signing key is configured (git config --get user.signingkey)\n\
|
||||
2. GPG agent is running\n\
|
||||
3. You can sign commits manually (try: git commit --amend -S)".to_string()
|
||||
} else {
|
||||
stdout.to_string()
|
||||
}
|
||||
} else {
|
||||
stderr.to_string()
|
||||
};
|
||||
|
||||
bail!("Failed to amend commit: {}", error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,15 +2,12 @@ use anyhow::{bail, Context, Result};
|
||||
use git2::{Repository, Signature, StatusOptions, Config, Oid, ObjectType};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use tempfile;
|
||||
|
||||
pub mod changelog;
|
||||
pub mod commit;
|
||||
pub mod tag;
|
||||
|
||||
pub use changelog::ChangelogGenerator;
|
||||
pub use commit::CommitBuilder;
|
||||
pub use tag::TagBuilder;
|
||||
|
||||
/// Git repository wrapper with enhanced cross-platform support
|
||||
pub struct GitRepo {
|
||||
repo: Repository,
|
||||
@@ -22,15 +19,41 @@ impl GitRepo {
|
||||
/// Open a git repository
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let absolute_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
|
||||
// Enhanced cross-platform path handling
|
||||
let absolute_path = if let Ok(canonical) = path.canonicalize() {
|
||||
canonical
|
||||
} else {
|
||||
// Fallback: convert to absolute path without canonicalization
|
||||
if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path)
|
||||
}
|
||||
};
|
||||
|
||||
// Try multiple git repository discovery strategies for cross-platform compatibility
|
||||
let repo = Repository::discover(&absolute_path)
|
||||
.or_else(|_| Repository::open(&absolute_path))
|
||||
.or_else(|discover_err| {
|
||||
// Try direct open as fallback
|
||||
Repository::open(&absolute_path).map_err(|open_err| {
|
||||
// Provide detailed error information for debugging
|
||||
anyhow::anyhow!(
|
||||
"Git repository discovery failed:\n\
|
||||
Discovery error: {}\n\
|
||||
Direct open error: {}\n\
|
||||
Path attempted: {:?}\n\
|
||||
Current directory: {:?}",
|
||||
discover_err, open_err, absolute_path, std::env::current_dir()
|
||||
)
|
||||
})
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to open git repository at '{:?}'. Please ensure:\n\
|
||||
1. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
|
||||
2. The path is correct and contains a valid '.git' folder.",
|
||||
1. The directory contains a valid '.git' folder\n\
|
||||
2. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
|
||||
3. You have proper permissions to access the repository",
|
||||
absolute_path,
|
||||
absolute_path.display()
|
||||
)
|
||||
@@ -341,19 +364,29 @@ impl GitRepo {
|
||||
let temp_file = tempfile::NamedTempFile::new()?;
|
||||
std::fs::write(temp_file.path(), message)?;
|
||||
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
cmd.args(&["commit", "-S", "-F", temp_file.path().to_str().unwrap()])
|
||||
.current_dir(&self.path)
|
||||
.output()?;
|
||||
|
||||
let output = std::process::Command::new("git")
|
||||
.args(&["commit", "-S", "-F", temp_file.path().to_str().unwrap()])
|
||||
.current_dir(&self.path)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to create signed commit: {}", stderr);
|
||||
|
||||
let error_msg = if stderr.is_empty() {
|
||||
if stdout.is_empty() {
|
||||
"GPG signing failed. Please check:\n\
|
||||
1. GPG signing key is configured (git config --get user.signingkey)\n\
|
||||
2. GPG agent is running\n\
|
||||
3. You can sign commits manually (try: git commit -S -m 'test')".to_string()
|
||||
} else {
|
||||
stdout.to_string()
|
||||
}
|
||||
} else {
|
||||
stderr.to_string()
|
||||
};
|
||||
|
||||
bail!("Failed to create signed commit: {}", error_msg);
|
||||
}
|
||||
|
||||
let head = self.repo.head()?;
|
||||
@@ -689,19 +722,37 @@ impl StatusSummary {
|
||||
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
||||
let start_path = start_path.as_ref();
|
||||
|
||||
// Try the starting path first
|
||||
if let Ok(repo) = GitRepo::open(start_path) {
|
||||
return Ok(repo);
|
||||
}
|
||||
|
||||
// Walk up the directory tree to find a git repository
|
||||
let mut current = start_path;
|
||||
let mut attempted_paths = vec![current.to_string_lossy().to_string()];
|
||||
|
||||
while let Some(parent) = current.parent() {
|
||||
attempted_paths.push(parent.to_string_lossy().to_string());
|
||||
|
||||
if let Ok(repo) = GitRepo::open(parent) {
|
||||
return Ok(repo);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
bail!("No git repository found starting from {:?}", start_path)
|
||||
// Provide detailed error information for debugging
|
||||
bail!(
|
||||
"No git repository found starting from {:?}.\n\
|
||||
Paths attempted:\n {}\n\
|
||||
Current directory: {:?}\n\
|
||||
Please ensure:\n\
|
||||
1. You are in a git repository or its subdirectory\n\
|
||||
2. The repository has a valid .git folder\n\
|
||||
3. You have proper permissions to access the repository",
|
||||
start_path,
|
||||
attempted_paths.join("\n "),
|
||||
std::env::current_dir()
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if path is inside a git repository
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::GitRepo;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use semver::Version;
|
||||
|
||||
/// Tag builder for creating tags
|
||||
|
||||
@@ -2,6 +2,4 @@ pub mod messages;
|
||||
pub mod translator;
|
||||
|
||||
pub use messages::Messages;
|
||||
pub use translator::Translator;
|
||||
pub use translator::translate_commit_type;
|
||||
pub use translator::translate_changelog_category;
|
||||
|
||||
@@ -70,10 +70,16 @@ impl AnthropicClient {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// List available models
|
||||
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||
// Anthropic doesn't have a models API endpoint, return predefined list
|
||||
Ok(ANTHROPIC_MODELS.iter().map(|&m| m.to_string()).collect())
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
let url = "https://api.anthropic.com/v1/messages";
|
||||
|
||||
|
||||
let request = MessagesRequest {
|
||||
model: self.model.clone(),
|
||||
max_tokens: 5,
|
||||
@@ -84,7 +90,7 @@ impl AnthropicClient {
|
||||
}],
|
||||
system: None,
|
||||
};
|
||||
|
||||
|
||||
let response = self.client
|
||||
.post(url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
@@ -93,7 +99,7 @@ impl AnthropicClient {
|
||||
.json(&request)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
|
||||
@@ -82,25 +82,53 @@ impl DeepSeekClient {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
/// List available models
|
||||
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||
let url = format!("{}/models", self.base_url);
|
||||
|
||||
|
||||
let response = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to validate DeepSeek API key")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else if response.status().as_u16() == 401 {
|
||||
Ok(false)
|
||||
} else {
|
||||
.context("Failed to list DeepSeek models")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
bail!("DeepSeek API error: {} - {}", status, text)
|
||||
bail!("DeepSeek API error: {} - {}", status, text);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<Model>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Model {
|
||||
id: String,
|
||||
}
|
||||
|
||||
let result: ModelsResponse = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse DeepSeek response")?;
|
||||
|
||||
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
match self.list_models().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,25 +82,53 @@ impl KimiClient {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
/// List available models
|
||||
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||
let url = format!("{}/models", self.base_url);
|
||||
|
||||
|
||||
let response = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to validate Kimi API key")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else if response.status().as_u16() == 401 {
|
||||
Ok(false)
|
||||
} else {
|
||||
.context("Failed to list Kimi models")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
bail!("Kimi API error: {} - {}", status, text)
|
||||
bail!("Kimi API error: {} - {}", status, text);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<Model>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Model {
|
||||
id: String,
|
||||
}
|
||||
|
||||
let result: ModelsResponse = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse Kimi response")?;
|
||||
|
||||
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
match self.list_models().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use crate::config::Language;
|
||||
|
||||
|
||||
@@ -82,10 +82,10 @@ impl OpenRouterClient {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
/// List available models
|
||||
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||
let url = format!("{}/models", self.base_url);
|
||||
|
||||
|
||||
let response = self.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
@@ -93,16 +93,44 @@ impl OpenRouterClient {
|
||||
.header("X-Title", "QuiCommit")
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to validate OpenRouter API key")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else if response.status().as_u16() == 401 {
|
||||
Ok(false)
|
||||
} else {
|
||||
.context("Failed to list OpenRouter models")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
bail!("OpenRouter API error: {} - {}", status, text)
|
||||
bail!("OpenRouter API error: {} - {}", status, text);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ModelsResponse {
|
||||
data: Vec<Model>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Model {
|
||||
id: String,
|
||||
}
|
||||
|
||||
let result: ModelsResponse = response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse OpenRouter response")?;
|
||||
|
||||
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||
}
|
||||
|
||||
/// Validate API key
|
||||
pub async fn validate_key(&self) -> Result<bool> {
|
||||
match self.list_models().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +239,7 @@ pub const OPENROUTER_MODELS: &[&str] = &[
|
||||
];
|
||||
|
||||
/// Check if a model name is valid
|
||||
pub fn is_valid_model(model: &str) -> bool {
|
||||
pub fn is_valid_model(_model: &str) -> bool {
|
||||
// Since OpenRouter supports many models, we'll allow any model name
|
||||
// but provide some popular ones as suggestions
|
||||
true
|
||||
|
||||
Reference in New Issue
Block a user