-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmod.rs
More file actions
222 lines (193 loc) · 7.42 KB
/
mod.rs
File metadata and controls
222 lines (193 loc) · 7.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use clap::Subcommand;
use crate::cli_client::CliClient;
use anyhow::Result;
use futures::StreamExt;
use rullm_core::LlmError;
use std::io::{self, Write};
use crate::spinner::Spinner;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub mod alias;
pub mod auth;
pub mod chat;
pub mod completions;
pub mod info;
pub mod templates;
pub mod models;
// Re-export the command args structs
pub use alias::AliasArgs;
pub use auth::AuthArgs;
pub use chat::ChatArgs;
pub use completions::CompletionsArgs;
pub use info::InfoArgs;
pub use models::ModelsArgs;
// Example strings for after_long_help
const CHAT_EXAMPLES: &str = r#"EXAMPLES:
rullm chat # Start chat with default model
rullm chat -m openai/gpt-4 # Chat with GPT-4
rullm chat -m claude # Chat using claude alias
rullm chat -m gemini/gemini-pro # Chat with Gemini Pro"#;
const MODELS_EXAMPLES: &str = r#"EXAMPLES:
rullm models list # List cached models
rullm models update # Fetch latest models
rullm models default openai/gpt-4o # Set default model
rullm models clear # Clear model cache"#;
const AUTH_EXAMPLES: &str = r#"EXAMPLES:
rullm auth login # Login interactively
rullm auth login anthropic # Login to Anthropic (OAuth or API key)
rullm auth logout openai # Logout from OpenAI
rullm auth list # Show all credentials"#;
const ALIAS_EXAMPLES: &str = r#"EXAMPLES:
rullm alias list # Show all available aliases
rullm alias add gpt4 openai/gpt-4 # Create custom alias
rullm alias show claude # Show alias details
rullm alias remove gpt4 # Remove custom alias"#;
const INFO_EXAMPLES: &str = r#"EXAMPLES:
rullm info # Show config paths and API key status"#;
const COMPLETIONS_EXAMPLES: &str = r#"EXAMPLES:
rullm completions bash > ~/.bashrc # Add bash completions
rullm completions zsh > ~/.zshrc # Add zsh completions
rullm completions fish > ~/.config/fish/completions/rullm.fish"#;
#[derive(Subcommand)]
pub enum Commands {
/// Start an interactive chat session
#[command(after_long_help = CHAT_EXAMPLES)]
Chat(ChatArgs),
/// Manage or inspect models
#[command(after_long_help = MODELS_EXAMPLES)]
Models(ModelsArgs),
/// Show configuration and system information
#[command(after_long_help = INFO_EXAMPLES)]
Info(InfoArgs),
/// Manage authentication credentials
#[command(after_long_help = AUTH_EXAMPLES)]
Auth(AuthArgs),
/// Manage model aliases
#[command(after_long_help = ALIAS_EXAMPLES)]
Alias(AliasArgs),
/// Generate shell completions
#[command(after_long_help = COMPLETIONS_EXAMPLES)]
Completions(CompletionsArgs),
/// Manage templates
#[command(
after_long_help = "EXAMPLES:\n rullm templates list\n rullm templates show code-review\n rullm templates remove old-template"
)]
Templates(templates::TemplatesArgs),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ModelsCache {
pub last_updated: DateTime<Utc>,
pub models: Vec<String>,
}
impl ModelsCache {
fn new(models: Vec<String>) -> Self {
Self {
last_updated: Utc::now(),
models,
}
}
}
/// Helper function to check environment variable status, eliminating duplication
fn env_var_status(var_name: &str) -> &'static str {
if std::env::var(var_name).is_ok() {
"Present"
} else {
"None"
}
}
pub async fn run_single_query(
client: &CliClient,
query: &str,
system_prompt: Option<&str>,
streaming: bool,
) -> Result<(), LlmError> {
if streaming {
// Use token-by-token streaming for real-time output
if let Some(_system) = system_prompt {
// TODO: Need to implement system prompt handling in streaming
// For now, fall back to non-streaming
let spinner = Spinner::new("Generating response");
spinner.start().await;
// Small delay to ensure spinner starts
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
match client.chat(query).await {
Ok(response) => {
spinner.stop_and_replace(&format!("{response}\n"));
}
Err(e) => {
spinner.stop_and_replace(&format!("Error: {e}\n"));
return Err(e);
}
}
} else {
// Show spinner while waiting for first token
let spinner = Spinner::new("Generating response");
spinner.start().await;
// Small delay to ensure spinner starts
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// Simple query streaming
let messages = vec![("user".to_string(), query.to_string())];
match client.stream_chat_raw(messages).await {
Ok(mut stream) => {
let mut first_token = true;
while let Some(result) = stream.next().await {
match result {
Ok(token) => {
if first_token {
spinner.stop();
first_token = false;
}
print!("{token}");
io::stdout()
.flush()
.map_err(|e| LlmError::unknown(e.to_string()))?;
}
Err(err) => {
spinner.stop_and_replace(&format!("Error: {err}\n"));
return Err(err);
}
}
}
println!(); // Final newline
// Ensure spinner is stopped if no tokens were received
if first_token {
spinner.stop_and_replace("(No response received)\n");
}
}
Err(e) => {
spinner.stop_and_replace(&format!("Error: {e}\n"));
return Err(e);
}
}
}
} else {
// Non-streaming path with spinner
let spinner = Spinner::new("Generating response");
spinner.start().await;
// Small delay to ensure spinner starts
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// TODO: Implement system prompt support properly
let result = client.chat(query).await;
match result {
Ok(response) => {
spinner.stop_and_replace(&format!("{response}\n"));
}
Err(e) => {
spinner.stop_and_replace(&format!("Error: {e}\n"));
return Err(e);
}
}
}
Ok(())
}
fn format_duration(duration: chrono::Duration) -> String {
let days = duration.num_days();
let hours = duration.num_hours() % 24;
match (days, hours) {
(0, h) => format!("{h}h"),
(1, 0) => "1 day".to_string(),
(1, h) => format!("1 day {h}h"),
(d, 0) => format!("{d} days"),
(d, h) => format!("{d} days {h}h"),
}
}