From 4961aae1737b77ff77102d37bf5f37f31a43824e Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 13 Jul 2026 17:21:21 +0200 Subject: fix assistant and senator Signed-off-by: salmonik --- src/commands/ai.rs | 18 ++--------- src/config.rs | 63 ++++++++++++++++++++++++++++++++++-- src/events/mod.rs | 18 ++++++----- src/lang.rs | 4 +-- src/openrouter/mod.rs | 88 ++++++++++++++++++++++++--------------------------- 5 files changed, 117 insertions(+), 74 deletions(-) diff --git a/src/commands/ai.rs b/src/commands/ai.rs index a75f164..5ab077e 100644 --- a/src/commands/ai.rs +++ b/src/commands/ai.rs @@ -4,7 +4,7 @@ use crate::lang; type Context<'a> = poise::Context<'a, Data, Error>; -/// Query the AI senator via OpenRouter +/// Query the AI senator via OpenRouter or local LLM #[poise::command(slash_command)] pub async fn senator( ctx: Context<'_>, @@ -13,20 +13,8 @@ pub async fn senator( ) -> crate::error::Result<()> { ctx.defer().await?; - let api_key = ctx - .data() - .config - .openrouter_api_key - .as_ref() - .ok_or_else(|| Error::Custom(lang::L_OPENROUTER_NO_KEY.into()))?; - let model = ctx - .data() - .config - .openrouter_model - .as_deref() - .unwrap_or("cognitivecomputations/dolphin-mistral-24b-venice-edition:free"); - - let client = crate::openrouter::OpenRouterClient::new(api_key.clone(), model.to_string()); + let (api_url, api_key, model) = ctx.data().config.resolve_llm(); + let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); let response = client.chat(lang::SENATOR_SYSTEM_PROMPT, &text).await?; if tts_vc.unwrap_or(false) { diff --git a/src/config.rs b/src/config.rs index 96a6849..989a060 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,8 +10,13 @@ pub struct Config { pub copyparty_url: String, pub copyparty_url_upload: String, pub copyparty_password: Option, + /// Deprecated – use llm_api_key instead pub openrouter_api_key: Option, + /// Deprecated – use llm_model instead pub openrouter_model: Option, + pub llm_api_key: Option, + pub llm_model: Option, + pub llm_api_url: String, pub database_path: String, } @@ -71,6 +76,24 @@ const ENV_VARS: &[EnvVar] = &[ default: Some("cognitivecomputations/dolphin-mistral-24b-venice-edition:free"), description: "OpenRouter model", }, + EnvVar { + name: "LLM_API_KEY", + required: false, + default: None, + description: "LLM API key (takes priority over OPENROUTER_API_KEY)", + }, + EnvVar { + name: "LLM_MODEL", + required: false, + default: None, + description: "LLM model name (takes priority over OPENROUTER_MODEL)", + }, + EnvVar { + name: "LLM_API_URL", + required: false, + default: Some("https://openrouter.ai/api/v1/chat/completions"), + description: "LLM API full URL including /chat/completions path", + }, EnvVar { name: "SENATOR_DB_PATH", required: false, @@ -126,6 +149,19 @@ pub fn check_env_vars() { impl Config { pub fn from_env() -> Self { + let openrouter_api_key = env::var("OPENROUTER_API_KEY").ok(); + let openrouter_model = env::var("OPENROUTER_MODEL").ok(); + let llm_api_key = env::var("LLM_API_KEY").ok(); + let llm_model = env::var("LLM_MODEL").ok(); + + // Warn about deprecated env vars (mirrors Python's BACKWARD_COMPAT_MSG) + if openrouter_api_key.is_some() && llm_api_key.is_none() { + tracing::warn!("OPENROUTER_API_KEY is deprecated. Use LLM_API_KEY instead."); + } + if openrouter_model.is_some() && llm_model.is_none() { + tracing::warn!("OPENROUTER_MODEL is deprecated. Use LLM_MODEL instead."); + } + Self { token: env::var("TOKEN").expect("Missing TOKEN"), status_channel_id: env::var("STATUS_CHANNEL_ID") @@ -137,9 +173,32 @@ impl Config { copyparty_url_upload: env::var("COPYPARTY_URL_UPLOAD") .unwrap_or_else(|_| "http://localhost:3939".into()), copyparty_password: env::var("COPYPARTY_PASSWORD").ok(), - openrouter_api_key: env::var("OPENROUTER_API_KEY").ok(), - openrouter_model: env::var("OPENROUTER_MODEL").ok(), + openrouter_api_key, + openrouter_model, + llm_api_key, + llm_model, + llm_api_url: env::var("LLM_API_URL") + .unwrap_or_else(|_| "https://openrouter.ai/api/v1/chat/completions".into()), database_path: env::var("SENATOR_DB_PATH").unwrap_or_else(|_| "data/senator.db".into()), } } + + /// Resolve the effective LLM config: (api_url, api_key, model). + /// LLM_* vars take priority; falls back to OPENROUTER_* for backward compat. + pub fn resolve_llm(&self) -> (String, String, String) { + let api_url = self.llm_api_url.clone(); + let api_key = self + .llm_api_key + .clone() + .or_else(|| self.openrouter_api_key.clone()) + .unwrap_or_default(); + let model = self + .llm_model + .clone() + .or_else(|| self.openrouter_model.clone()) + .unwrap_or_else(|| { + "cognitivecomputations/dolphin-mistral-24b-venice-edition:free".into() + }); + (api_url, api_key, model) + } } diff --git a/src/events/mod.rs b/src/events/mod.rs index 457ebcd..9a193d0 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -335,20 +335,22 @@ pub async fn handle_message( let is_mention = new_message.mentions_user_id(ctx.cache.current_user().id); let has_keyword = content_lower.contains(&data.config.assistant_keyword); - if (is_mention || has_keyword) && data.config.openrouter_api_key.is_some() { - let api_key = data.config.openrouter_api_key.clone().unwrap(); - let model = data.config.openrouter_model.clone().unwrap_or_else(|| { - "cognitivecomputations/dolphin-mistral-24b-venice-edition:free".into() - }); - - let client = crate::openrouter::OpenRouterClient::new(api_key, model); + if is_mention || has_keyword { + let (api_url, api_key, model) = data.config.resolve_llm(); + let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT; + let bot_id = ctx.cache.current_user().id; let mut messages = Vec::new(); let mut current_msg = Some(new_message.clone()); for _ in 0..5 { if let Some(msg) = current_msg { - messages.push(("user".into(), msg.content.clone())); + let role = if msg.author.id == bot_id { + "assistant" + } else { + "user" + }; + messages.push((role.into(), msg.content.clone())); current_msg = match msg.message_reference.and_then(|r| r.message_id) { Some(ref_id) => msg.channel_id.message(ctx, ref_id).await.ok(), None => None, diff --git a/src/lang.rs b/src/lang.rs index 626f387..b1ed088 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -312,8 +312,8 @@ pub fn fmt_cp_invalid_json(e: &dyn std::fmt::Display, snippet: &str) -> String { // ── OpenRouter ──────────────────────────────────────────────────────── -pub fn fmt_openrouter_err(status: &dyn std::fmt::Display, text: &str) -> String { - format!("Błąd API OpenRouter. {status}: {text}") +pub fn fmt_llm_err(endpoint: &str, status: &dyn std::fmt::Display, text: &str) -> String { + format!("Błąd API LLM ({endpoint}). {status}: {text}") } // ── Voice / audio decoding ──────────────────────────────────────────── diff --git a/src/openrouter/mod.rs b/src/openrouter/mod.rs index 31cee85..1d3b443 100644 --- a/src/openrouter/mod.rs +++ b/src/openrouter/mod.rs @@ -1,3 +1,5 @@ +use tracing::info; + use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -7,6 +9,7 @@ use crate::lang; pub struct OpenRouterClient { api_key: String, model: String, + api_url: String, client: Client, } @@ -39,14 +42,49 @@ struct MessageContent { } impl OpenRouterClient { - pub fn new(api_key: String, model: String) -> Self { + /// `api_url` is the full URL including `/chat/completions` path. + pub fn new(api_url: String, api_key: String, model: String) -> Self { + info!("LLM client: url={api_url}, model={model}"); Self { api_key, model, + api_url, client: Client::new(), } } + async fn do_request(&self, body: &ChatRequest) -> Result { + let mut req = self + .client + .post(&self.api_url) + .header("Content-Type", "application/json") + .json(body); + + if !self.api_key.is_empty() { + req = req.header("Authorization", format!("Bearer {}", self.api_key)); + } + + let resp = req.send().await?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + return Err(crate::error::Error::Custom(lang::fmt_llm_err( + &self.api_url, + &status, + &text, + ))); + } + + let chat_resp: ChatResponse = resp.json().await?; + Ok(chat_resp + .choices + .into_iter() + .next() + .map(|c| c.message.content) + .unwrap_or_default()) + } + pub async fn chat(&self, system_prompt: &str, user_message: &str) -> Result { let body = ChatRequest { model: self.model.clone(), @@ -63,29 +101,7 @@ impl OpenRouterClient { max_tokens: 500, }; - let resp = self - .client - .post("https://openrouter.ai/api/v1/chat/completions") - .header("Authorization", format!("Bearer {}", self.api_key)) - .json(&body) - .send() - .await?; - - if !resp.status().is_success() { - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - return Err(crate::error::Error::Custom(lang::fmt_openrouter_err( - &status, &text, - ))); - } - - let chat_resp: ChatResponse = resp.json().await?; - Ok(chat_resp - .choices - .into_iter() - .next() - .map(|c| c.message.content) - .unwrap_or_default()) + self.do_request(&body).await } pub async fn chat_with_history( @@ -107,28 +123,6 @@ impl OpenRouterClient { max_tokens: 500, }; - let resp = self - .client - .post("https://openrouter.ai/api/v1/chat/completions") - .header("Authorization", format!("Bearer {}", self.api_key)) - .json(&body) - .send() - .await?; - - if !resp.status().is_success() { - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - return Err(crate::error::Error::Custom(lang::fmt_openrouter_err( - &status, &text, - ))); - } - - let chat_resp: ChatResponse = resp.json().await?; - Ok(chat_resp - .choices - .into_iter() - .next() - .map(|c| c.message.content) - .unwrap_or_default()) + self.do_request(&body).await } } -- cgit v1.2.3