Files
dm-pal/src-tauri/src/llm/mod.rs
T
itsamejms 1e7301647d
CI / frontend (push) Successful in 30s
CI / rust (push) Successful in 5m42s
feat: explicit provider field replaces URL sniffing
LlmConfig.provider ("ollama" | "openai", empty = sniff URL so legacy
configs keep working). Settings presets set it — a custom-port Ollama
no longer falls into the OpenAI branch and fails confusingly.

Also: test_connection now accepts an optional config override — the
wizard was passing one that Rust silently ignored, so it tested the
saved config instead of the URL the user just typed.
2026-09-06 23:27:20 +01:00

140 lines
4.7 KiB
Rust

use serde::{Deserialize, Serialize};
use std::sync::Mutex;
// ─── LLM Event (for streaming) ───────────────────────────────
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "camelCase")]
pub enum LlmEvent {
Token(String),
Done(String), // full text
Error(String),
}
// ─── App State ────────────────────────────────────────────────
pub struct AppState {
pub config: Mutex<LlmConfig>,
pub rag: crate::rag::RagStore,
pub gen: crate::generations::GenerationStore,
/// Where lore.db, generations.db, and cached images live. Configurable via
/// the `dataDir` key in the `dm-pal-prefs.json` store; defaults to
/// `$APPDATA/dm-toolkit` when unset. Set once at startup.
pub data_dir: std::path::PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
/// API dialect: "ollama" (native /api/*) or "openai" (/v1/*).
/// Empty = sniff the URL (legacy configs + defaults). Provider presets
/// set it explicitly — a custom-port Ollama would otherwise be sniffed
/// as OpenAI and fail confusingly. serde default: old stored configs.
#[serde(default)]
pub provider: String,
pub api_url: String,
pub api_key: String,
pub model: String,
pub temperature: f32,
pub max_tokens: u32,
pub top_p: f32,
/// Base URL of a stable-diffusion.cpp `sd-server` (A1111-compatible API),
/// e.g. `http://localhost:1234`. Cross-platform — no longer Ollama/macOS-only.
pub image_api_url: String,
/// Ollama embedding model for lore RAG (e.g. `nomic-embed-text`).
pub embed_model: String,
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
// Default to Ollama local server; also works with LM Studio, llama.cpp server, or OpenAI
provider: String::new(),
api_url: "http://localhost:11434".to_string(),
api_key: String::new(),
model: "llama3.2".to_string(),
temperature: 0.7,
max_tokens: 512,
top_p: 0.9,
image_api_url: "http://localhost:1234".to_string(),
embed_model: "nomic-embed-text".to_string(),
}
}
}
// ─── Generation Request ───────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct GenerateRequest {
pub prompt: String,
pub system: Option<String>,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
/// Optional RAG query: when set, the top lore chunks for this query are
/// retrieved and prepended to the system prompt so generation stays
/// consistent with the user's world bible.
pub rag_query: Option<String>,
}
// ─── OpenAI-Compatible Chat Response ─────────────────────────
#[derive(Debug, Deserialize)]
pub struct ChatResponse {
pub choices: Vec<ChatChoice>,
}
#[derive(Debug, Deserialize)]
pub struct ChatChoice {
pub message: ChatMessage,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
// ─── Ollama Generate Response ─────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct OllamaGenerateResponse {
pub response: String,
pub done: bool,
}
#[derive(Debug, Deserialize)]
pub struct OllamaChatResponse {
pub message: ChatMessage,
pub done: bool,
}
// ─── Helper: are we talking Ollama? ───────────────────────
/// True when we should speak Ollama's native /api/* protocol. An explicit
/// provider on the config wins; empty falls back to URL sniffing so legacy
/// and default configs keep working.
pub fn is_ollama(provider: &str, url: &str) -> bool {
if provider.eq_ignore_ascii_case("ollama") {
return true;
}
if provider.eq_ignore_ascii_case("openai") {
return false;
}
url.contains("localhost:11434") || url.contains("127.0.0.1:11434")
}
// ─── Build system + user messages from a prompt ──────────────
pub fn build_messages(system: Option<&str>, prompt: &str) -> Vec<ChatMessage> {
let mut messages = Vec::new();
if let Some(sys) = system {
messages.push(ChatMessage {
role: "system".to_string(),
content: sys.to_string(),
});
}
messages.push(ChatMessage {
role: "user".to_string(),
content: prompt.to_string(),
});
messages
}