refactor(ai): read LLM settings from LlmConfig instead of env
Every LLM env reader now goes through ai::config::llm_config(): build_ollama / build_llamacpp / build_openrouter take config sections (OPENROUTER_API_KEY is still read from env), and the backend switch, embedding dim + prefixes, Ollama / llama.cpp / TTS timeouts, TTS voice and ref length, agentic caps, chat turn timeout and the unified-search model read their sections. LocalLlm::from_env -> from_config; the test_daily_summary and populate_knowledge bins take Ollama defaults from the config. env_max_iterations / env_default_num_ctx are renamed to max_chat_iterations / default_num_ctx. The picker still uses picker.include (= LLAMA_SWAP_ALLOWED_MODELS) until discovery lands. Legacy env vars keep working through the config's override layer; checked against the deployed .env, which resolves to the same values. The LLM_BACKEND parsing tests move to ai::config. llm.example.toml now leaves the embed prefixes empty: copying the nomic prefixes onto a corpus embedded without them breaks search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b8a99a2b2
commit
aa3536afee
17 files changed
+144
-292
No files matched your search
+38
-87
@@ -1,5 +1,6 @@
|
||||
use crate::ai::apollo_client::ApolloClient;
|
||||
use crate::ai::clip_client::ClipClient;
|
||||
use crate::ai::config::{LlmConfig, OllamaConfig, OpenRouterConfig, llm_config};
|
||||
use crate::ai::face_client::FaceClient;
|
||||
use crate::ai::insight_chat::{ChatLockMap, InsightChatService};
|
||||
use crate::ai::llamacpp::LlamaCppClient;
|
||||
@@ -224,13 +225,14 @@ impl AppState {
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
// Initialize AI clients
|
||||
let ollama = build_ollama_from_env();
|
||||
let llm = llm_config();
|
||||
let ollama = build_ollama(&llm.ollama);
|
||||
|
||||
let openrouter = build_openrouter_from_env();
|
||||
let openrouter_allowed_models = parse_openrouter_allowed_models();
|
||||
let openrouter = build_openrouter(&llm.openrouter);
|
||||
let openrouter_allowed_models = llm.openrouter.allowed.clone();
|
||||
|
||||
let llamacpp = build_llamacpp_from_env();
|
||||
let llamacpp_allowed_models = parse_llamacpp_allowed_models();
|
||||
let llamacpp = build_llamacpp(llm);
|
||||
let llamacpp_allowed_models = llm.llamacpp.picker.include.clone().unwrap_or_default();
|
||||
|
||||
let sms_api_url =
|
||||
env::var("SMS_API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
|
||||
@@ -346,10 +348,7 @@ impl Default for AppState {
|
||||
|
||||
// Turn registry for reconnectable chat turns. 5-minute timeout for
|
||||
// stale turns (background cleaner drops entries older than this).
|
||||
let timeout_secs: u64 = env::var("INSIGHT_CHAT_TURN_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(300);
|
||||
let timeout_secs = llm_config().agentic.chat_turn_timeout_secs;
|
||||
let turn_registry = Arc::new(TurnRegistry::new(timeout_secs));
|
||||
|
||||
// Ensure preview clips directory exists
|
||||
@@ -387,94 +386,46 @@ impl Default for AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an `OpenRouterClient` from environment variables. Returns `None`
|
||||
/// when `OPENROUTER_API_KEY` is unset (the hybrid backend is then
|
||||
/// unavailable and requests for it return a clear error).
|
||||
fn build_openrouter_from_env() -> Option<Arc<OpenRouterClient>> {
|
||||
/// Build an `OpenRouterClient` from `[openrouter]`. Returns `None` when
|
||||
/// `OPENROUTER_API_KEY` (env-only, it's a secret) is unset — the hybrid
|
||||
/// backend is then unavailable and requests for it return a clear error.
|
||||
fn build_openrouter(cfg: &OpenRouterConfig) -> Option<Arc<OpenRouterClient>> {
|
||||
let api_key = env::var("OPENROUTER_API_KEY").ok()?;
|
||||
let base_url = env::var("OPENROUTER_BASE_URL").ok();
|
||||
let default_model = env::var("OPENROUTER_DEFAULT_MODEL")
|
||||
.unwrap_or_else(|_| "anthropic/claude-sonnet-4".to_string());
|
||||
let mut client = OpenRouterClient::new(api_key, base_url, default_model);
|
||||
client.set_attribution(
|
||||
env::var("OPENROUTER_HTTP_REFERER").ok(),
|
||||
env::var("OPENROUTER_APP_TITLE").ok(),
|
||||
);
|
||||
if let Ok(model) = env::var("OPENROUTER_EMBEDDING_MODEL") {
|
||||
client.set_embedding_model(model);
|
||||
}
|
||||
// OPENROUTER_MAX_TOKENS=0 omits the cap (provider default).
|
||||
if let Some(n) = env::var("OPENROUTER_MAX_TOKENS")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u32>().ok())
|
||||
{
|
||||
client.set_max_tokens((n > 0).then_some(n));
|
||||
let mut client =
|
||||
OpenRouterClient::new(api_key, cfg.base_url.clone(), cfg.default_model.clone());
|
||||
client.set_attribution(cfg.http_referer.clone(), cfg.app_title.clone());
|
||||
if let Some(model) = &cfg.embedding_model {
|
||||
client.set_embedding_model(model.clone());
|
||||
}
|
||||
client.set_max_tokens(cfg.max_tokens_cap());
|
||||
Some(Arc::new(client))
|
||||
}
|
||||
|
||||
/// Parse `OPENROUTER_ALLOWED_MODELS` (comma-separated) into a vec. Returns
|
||||
/// empty when unset, in which case `/insights/openrouter/models` reports no
|
||||
/// curated picks and the server falls back to `OPENROUTER_DEFAULT_MODEL`.
|
||||
fn parse_openrouter_allowed_models() -> Vec<String> {
|
||||
env::var("OPENROUTER_ALLOWED_MODELS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
/// Build the `OllamaClient` from `[ollama]` — shared by the server
|
||||
/// (`AppState::default`) and the standalone binaries.
|
||||
pub fn build_ollama(cfg: &OllamaConfig) -> OllamaClient {
|
||||
OllamaClient::new(
|
||||
cfg.primary_url.clone(),
|
||||
cfg.fallback_url.clone(),
|
||||
cfg.primary_model.clone(),
|
||||
cfg.fallback_model.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the `OllamaClient` from environment variables — the canonical
|
||||
/// `OLLAMA_*` wiring shared by the server (`AppState::default`) and the
|
||||
/// standalone binaries (which predate this helper and used to copy it).
|
||||
pub fn build_ollama_from_env() -> OllamaClient {
|
||||
let primary_url = env::var("OLLAMA_PRIMARY_URL").unwrap_or_else(|_| {
|
||||
env::var("OLLAMA_URL").unwrap_or_else(|_| "http://localhost:11434".to_string())
|
||||
});
|
||||
let fallback_url = env::var("OLLAMA_FALLBACK_URL").ok();
|
||||
let primary_model = env::var("OLLAMA_PRIMARY_MODEL")
|
||||
.or_else(|_| env::var("OLLAMA_MODEL"))
|
||||
.unwrap_or_else(|_| "nemotron-3-nano:30b".to_string());
|
||||
let fallback_model = env::var("OLLAMA_FALLBACK_MODEL").ok();
|
||||
|
||||
OllamaClient::new(primary_url, fallback_url, primary_model, fallback_model)
|
||||
}
|
||||
|
||||
/// Build a `LlamaCppClient` from environment variables. Returns `None` when
|
||||
/// `LLAMA_SWAP_URL` is unset. The client is constructed unconditionally
|
||||
/// when the URL is set (so it's available even under `LLM_BACKEND=ollama`
|
||||
/// for ad-hoc tooling), but the agentic / chat paths only route through it
|
||||
/// when `LLM_BACKEND=llamacpp`. Slot ids default to the names the bundled
|
||||
/// `llama-swap/config.yaml` uses — `chat` / `vision` / `embed`.
|
||||
pub fn build_llamacpp_from_env() -> Option<Arc<LlamaCppClient>> {
|
||||
let base_url = env::var("LLAMA_SWAP_URL").ok()?;
|
||||
let primary_model = env::var("LLAMA_SWAP_PRIMARY_MODEL").ok();
|
||||
let mut client = LlamaCppClient::new(Some(base_url), primary_model);
|
||||
if let Ok(model) = env::var("LLAMA_SWAP_EMBEDDING_MODEL") {
|
||||
client.set_embedding_model(model);
|
||||
}
|
||||
if let Ok(model) = env::var("LLAMA_SWAP_VISION_MODEL") {
|
||||
client.set_vision_model(model);
|
||||
}
|
||||
if let Ok(model) = env::var("LLAMA_SWAP_TTS_MODEL") {
|
||||
client.set_tts_model(model);
|
||||
}
|
||||
/// Build a `LlamaCppClient` from `[llamacpp]` (+ the embed and TTS model
|
||||
/// ids). Returns `None` when `llamacpp.url` is unset. The client is built
|
||||
/// whenever the URL is set (TTS and ad-hoc tooling use it even under
|
||||
/// `backend = "ollama"`), but chat / embeddings only route through it when
|
||||
/// `backend = "llamacpp"`.
|
||||
pub fn build_llamacpp(cfg: &LlmConfig) -> Option<Arc<LlamaCppClient>> {
|
||||
let base_url = cfg.llamacpp.url.clone()?;
|
||||
let mut client = LlamaCppClient::new(Some(base_url), Some(cfg.llamacpp.chat.clone()));
|
||||
client.set_embedding_model(cfg.embedding.model.clone());
|
||||
client.set_vision_model(cfg.llamacpp.vision_model().to_string());
|
||||
client.set_tts_model(cfg.tts.model.clone());
|
||||
Some(Arc::new(client))
|
||||
}
|
||||
|
||||
/// Parse `LLAMA_SWAP_ALLOWED_MODELS` (comma-separated) into a vec. Used to
|
||||
/// populate the model picker when `LLM_BACKEND=llamacpp` — `/insights/models`
|
||||
/// surfaces these slots with capabilities. Empty when unset.
|
||||
fn parse_llamacpp_allowed_models() -> Vec<String> {
|
||||
env::var("LLAMA_SWAP_ALLOWED_MODELS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl AppState {
|
||||
/// Creates an AppState instance for testing with temporary directories
|
||||
|
||||
Reference in new issue
Block a user