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.
This commit is contained in:
@@ -37,7 +37,7 @@ async fn generate_inner(state: tauri::State<'_, AppState>, req: GenerateRequest)
|
|||||||
// consistent with the user's world bible. Shared path = every caller.
|
// consistent with the user's world bible. Shared path = every caller.
|
||||||
let messages = inject_lore(&state, &client, &config, messages, &req.rag_query).await;
|
let messages = inject_lore(&state, &client, &config, messages, &req.rag_query).await;
|
||||||
|
|
||||||
if llm::is_ollama(&config.api_url) {
|
if llm::is_ollama(&config.provider, &config.api_url) {
|
||||||
call_ollama(&client, &config, &messages, temperature, max_tokens).await
|
call_ollama(&client, &config, &messages, temperature, max_tokens).await
|
||||||
} else {
|
} else {
|
||||||
call_openai(&client, &config, &messages, temperature, max_tokens).await
|
call_openai(&client, &config, &messages, temperature, max_tokens).await
|
||||||
@@ -65,7 +65,7 @@ pub async fn generate_stream(
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
// For now, we do a non-streaming call and emit the full response as one token
|
// For now, we do a non-streaming call and emit the full response as one token
|
||||||
// Real SSE streaming from Ollama/OpenAI can be added later
|
// Real SSE streaming from Ollama/OpenAI can be added later
|
||||||
let result = if llm::is_ollama(&config.api_url) {
|
let result = if llm::is_ollama(&config.provider, &config.api_url) {
|
||||||
call_ollama(&client, &config, &messages, temperature, max_tokens).await
|
call_ollama(&client, &config, &messages, temperature, max_tokens).await
|
||||||
} else {
|
} else {
|
||||||
call_openai(&client, &config, &messages, temperature, max_tokens).await
|
call_openai(&client, &config, &messages, temperature, max_tokens).await
|
||||||
@@ -123,14 +123,22 @@ pub struct ConnectionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_connection(state: tauri::State<'_, AppState>) -> Result<ConnectionTest, String> {
|
pub async fn test_connection(
|
||||||
let config = state.config.lock().map_err(|e| e.to_string())?.clone();
|
state: tauri::State<'_, AppState>,
|
||||||
|
config: Option<crate::llm::LlmConfig>,
|
||||||
|
) -> Result<ConnectionTest, String> {
|
||||||
|
// Optional override: the first-run wizard tests a config it hasn't saved
|
||||||
|
// yet. None = use the persisted state (Settings panel saves first anyway).
|
||||||
|
let config = match config {
|
||||||
|
Some(c) => c,
|
||||||
|
None => state.config.lock().map_err(|e| e.to_string())?.clone(),
|
||||||
|
};
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(8))
|
.timeout(std::time::Duration::from_secs(8))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if llm::is_ollama(&config.api_url) {
|
if llm::is_ollama(&config.provider, &config.api_url) {
|
||||||
let url = format!("{}/api/tags", config.api_url.trim_end_matches('/'));
|
let url = format!("{}/api/tags", config.api_url.trim_end_matches('/'));
|
||||||
let res = client.get(&url).send().await.map_err(|e| e.to_string())?;
|
let res = client.get(&url).send().await.map_err(|e| e.to_string())?;
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ pub struct AppState {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LlmConfig {
|
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_url: String,
|
||||||
pub api_key: String,
|
pub api_key: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
@@ -42,6 +48,7 @@ impl Default for LlmConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// Default to Ollama local server; also works with LM Studio, llama.cpp server, or OpenAI
|
// 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_url: "http://localhost:11434".to_string(),
|
||||||
api_key: String::new(),
|
api_key: String::new(),
|
||||||
model: "llama3.2".to_string(),
|
model: "llama3.2".to_string(),
|
||||||
@@ -100,9 +107,18 @@ pub struct OllamaChatResponse {
|
|||||||
pub done: bool,
|
pub done: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helper: detect if we're talking to Ollama ───────────────
|
// ─── Helper: are we talking Ollama? ───────────────────────
|
||||||
|
|
||||||
pub fn is_ollama(url: &str) -> bool {
|
/// 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")
|
url.contains("localhost:11434") || url.contains("127.0.0.1:11434")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
|
|||||||
// Test connection to the default Ollama URL
|
// Test connection to the default Ollama URL
|
||||||
const result = await invoke<{ ok: boolean; models: string[]; error: string }>(
|
const result = await invoke<{ ok: boolean; models: string[]; error: string }>(
|
||||||
"test_connection",
|
"test_connection",
|
||||||
{ config: { api_url: apiUrl, api_key: apiKey, model: "", temperature: 0.7, max_tokens: 512, top_p: 0.9, image_api_url: "", embed_model: "" } }
|
{ config: { provider: "ollama", api_url: apiUrl, api_key: apiKey, model: "", temperature: 0.7, max_tokens: 512, top_p: 0.9, image_api_url: "", embed_model: "" } }
|
||||||
);
|
);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
setModels(result.models);
|
setModels(result.models);
|
||||||
@@ -52,6 +52,7 @@ export function FirstRunWizard({ onComplete }: { onComplete: () => void }) {
|
|||||||
// We'll just set the config and complete.
|
// We'll just set the config and complete.
|
||||||
await invoke("set_llm_config", {
|
await invoke("set_llm_config", {
|
||||||
config: {
|
config: {
|
||||||
|
provider: "ollama",
|
||||||
api_url: apiUrl,
|
api_url: apiUrl,
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
model: model,
|
model: model,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { open } from "@tauri-apps/plugin-dialog";
|
|||||||
import { useToast } from "./Toast";
|
import { useToast } from "./Toast";
|
||||||
|
|
||||||
interface LlmConfig {
|
interface LlmConfig {
|
||||||
|
/** "ollama" | "openai" — empty = let the backend sniff the URL. */
|
||||||
|
provider?: string;
|
||||||
api_url: string;
|
api_url: string;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
model: string;
|
model: string;
|
||||||
@@ -174,16 +176,17 @@ export function SettingsPanel() {
|
|||||||
setImgTesting(false);
|
setImgTesting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ponytail: provider presets fill in the API URL pattern + default model.
|
// ponytail: provider presets fill in the API URL pattern + default model
|
||||||
const PRESETS: { label: string; url: string; model: string; key?: boolean }[] = [
|
// + the API dialect (provider), so a custom-port Ollama isn't sniffed wrong.
|
||||||
{ label: "Ollama", url: "http://localhost:11434", model: "llama3.2" },
|
const PRESETS: { label: string; url: string; model: string; provider: string; key?: boolean }[] = [
|
||||||
{ label: "LM Studio", url: "http://localhost:1234/v1", model: "local-model" },
|
{ label: "Ollama", url: "http://localhost:11434", model: "llama3.2", provider: "ollama" },
|
||||||
{ label: "OpenAI", url: "https://api.openai.com", model: "gpt-4o-mini", key: true },
|
{ label: "LM Studio", url: "http://localhost:1234/v1", model: "local-model", provider: "openai" },
|
||||||
{ label: "Custom", url: "", model: "" },
|
{ label: "OpenAI", url: "https://api.openai.com", model: "gpt-4o-mini", provider: "openai", key: true },
|
||||||
|
{ label: "Custom", url: "", model: "", provider: "" },
|
||||||
];
|
];
|
||||||
function applyPreset(p: { url: string; model: string }) {
|
function applyPreset(p: { url: string; model: string; provider: string }) {
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
setConfig({ ...config, api_url: p.url, model: p.model });
|
setConfig({ ...config, api_url: p.url, model: p.model, provider: p.provider });
|
||||||
setConn(null);
|
setConn(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +195,7 @@ export function SettingsPanel() {
|
|||||||
// backend re-default on save — but we can't send a partial, so mirror the
|
// backend re-default on save — but we can't send a partial, so mirror the
|
||||||
// known defaults here and toast.
|
// known defaults here and toast.
|
||||||
const DEFAULTS: LlmConfig = {
|
const DEFAULTS: LlmConfig = {
|
||||||
|
provider: "",
|
||||||
api_url: "http://localhost:11434",
|
api_url: "http://localhost:11434",
|
||||||
api_key: "",
|
api_key: "",
|
||||||
model: "llama3.2",
|
model: "llama3.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user