Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions openless-all/app/src-tauri/src/commands/credentials.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::*;

const LLM_EXTRA_HEADERS_ACCOUNT: &str = "ark.extra_headers";
const LLM_TEMPERATURE_ACCOUNT: &str = "ark.temperature";

#[tauri::command]
pub fn get_credentials() -> CredentialsStatus {
Expand Down Expand Up @@ -171,6 +172,11 @@ pub fn set_credential(
let _ = window.emit("credentials:changed", ());
return Ok(());
}
if account == LLM_TEMPERATURE_ACCOUNT {
CredentialsVault::set_active_llm_temperature(&value).map_err(|e| e.to_string())?;
let _ = window.emit("credentials:changed", ());
return Ok(());
}
let acc = parse_account(&account)?;
if let Some(provider) = provider {
if !matches!(
Expand Down Expand Up @@ -276,6 +282,9 @@ pub fn read_credential(
if account == LLM_EXTRA_HEADERS_ACCOUNT {
return CredentialsVault::get_active_llm_extra_headers_json().map_err(|e| e.to_string());
}
if account == LLM_TEMPERATURE_ACCOUNT {
return Ok(CredentialsVault::get_active_llm_temperature_string());
}
let acc = parse_account(&account)?;
if let Some(provider) = provider {
CredentialsVault::get_for_asr_provider(&provider, acc).map_err(|e| e.to_string())
Expand Down
8 changes: 5 additions & 3 deletions openless-all/app/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ pub(crate) use crate::persistence::{
PreferencesStore,
};
pub(crate) use crate::polish::{
http_client_builder, CodexOAuthConfig, CodexOAuthCredentials, CodexOAuthLLMProvider, LLMError,
OpenAICompatibleConfig, OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL,
CODEX_OAUTH_PROVIDER_ID,
http_client_builder, openai_compatible_temperature_for_provider, CodexOAuthConfig,
CodexOAuthCredentials, CodexOAuthLLMProvider, LLMError, OpenAICompatibleConfig,
OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL, CODEX_OAUTH_PROVIDER_ID,
};
#[cfg(not(mobile))]
pub(crate) use crate::recorder::{AudioConsumer, Recorder};
Expand Down Expand Up @@ -1377,6 +1377,7 @@ mod tests {
base_url: format!("http://{}", addr),
api_key: String::new(),
extra_headers: Default::default(),
temperature: None,
})
.await
.unwrap();
Expand Down Expand Up @@ -1425,6 +1426,7 @@ mod tests {
extra_headers: [("x-openless-test-token".to_string(), "secret".to_string())]
.into_iter()
.collect(),
temperature: None,
})
.await
.unwrap();
Expand Down
18 changes: 15 additions & 3 deletions openless-all/app/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub(crate) struct ProviderConfig {
pub(crate) base_url: String,
pub(crate) api_key: String,
pub(crate) extra_headers: HashMap<String, String>,
pub(crate) temperature: Option<f32>,
}

fn read_openai_provider_config(kind: &str) -> Result<ProviderConfig, String> {
Expand All @@ -132,10 +133,17 @@ fn read_openai_provider_config(kind: &str) -> Result<ProviderConfig, String> {
let base_url = CredentialsVault::get(endpoint_account)
.map_err(|e| e.to_string())?
.unwrap_or_default();
let extra_headers = if kind == "llm" {
CredentialsVault::get_active_llm_extra_headers()
let (extra_headers, temperature) = if kind == "llm" {
let active_llm = CredentialsVault::get_active_llm();
(
CredentialsVault::get_active_llm_extra_headers(),
openai_compatible_temperature_for_provider(
&active_llm,
CredentialsVault::get_active_llm_temperature(),
),
)
} else {
HashMap::new()
(HashMap::new(), None)
};
if api_key_required && api_key.trim().is_empty() {
return Err("API Key 为空".to_string());
Expand All @@ -154,6 +162,7 @@ fn read_openai_provider_config(kind: &str) -> Result<ProviderConfig, String> {
base_url,
api_key,
extra_headers,
temperature,
})
}

Expand Down Expand Up @@ -202,6 +211,7 @@ async fn validate_llm_provider() -> Result<(), String> {
model,
)
.with_thinking_enabled(llm_thinking_enabled)
.with_temperature(config.temperature)
.with_extra_headers(config.extra_headers),
);
provider
Expand Down Expand Up @@ -1021,6 +1031,7 @@ mod tests {
),
api_key: String::new(),
extra_headers: HashMap::new(),
temperature: None,
})
.await
.expect_err("closed listener should reject the provider request");
Expand Down Expand Up @@ -1084,6 +1095,7 @@ mod tests {
base_url: format!("http://{addr}/v1?token=query-secret#client-fragment"),
api_key: String::new(),
extra_headers: HashMap::new(),
temperature: None,
})
.await
.unwrap();
Expand Down
10 changes: 8 additions & 2 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ use crate::persistence::{

use crate::llm_gemini::{GeminiConfig, GeminiProvider};
use crate::polish::{
ActiveLLMProvider, CodexOAuthConfig, CodexOAuthLLMProvider, OpenAICompatibleConfig,
OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL, CODEX_OAUTH_PROVIDER_ID,
openai_compatible_temperature_for_provider, ActiveLLMProvider, CodexOAuthConfig,
CodexOAuthLLMProvider, OpenAICompatibleConfig, OpenAICompatibleLLMProvider,
CODEX_DEFAULT_MODEL, CODEX_OAUTH_PROVIDER_ID,
};
use crate::qa_hotkey::{QaHotkeyError, QaHotkeyEvent, QaHotkeyMonitor};
use crate::recorder::{Recorder, RecorderError};
Expand Down Expand Up @@ -2589,8 +2590,13 @@ fn build_active_llm_provider(llm_thinking_enabled: bool) -> anyhow::Result<Activ
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();
let temperature = openai_compatible_temperature_for_provider(
&active,
CredentialsVault::get_active_llm_temperature(),
);
let config = OpenAICompatibleConfig::new(active, "OpenLess LLM", base_url, api_key, model)
.with_thinking_enabled(llm_thinking_enabled)
.with_temperature(temperature)
.with_extra_headers(CredentialsVault::get_active_llm_extra_headers());
Ok(ActiveLLMProvider::OpenAI(OpenAICompatibleLLMProvider::new(
config,
Expand Down
104 changes: 103 additions & 1 deletion openless-all/app/src-tauri/src/persistence/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,26 @@ fn active_llm_extra_headers(root: &CredsRoot) -> HashMap<String, String> {
.unwrap_or_default()
}

fn is_valid_llm_temperature(temperature: f64) -> bool {
temperature.is_finite() && (0.0..=2.0).contains(&temperature)
}

fn active_llm_temperature_value(root: &CredsRoot) -> Option<f64> {
root.providers
.llm
.get(&root.active.llm)
.and_then(|entry| entry.temperature)
.filter(|temperature| is_valid_llm_temperature(*temperature))
}

fn active_llm_temperature(root: &CredsRoot) -> Option<f32> {
active_llm_temperature_value(root).map(|temperature| temperature as f32)
}

fn active_llm_temperature_string(root: &CredsRoot) -> Option<String> {
active_llm_temperature_value(root).map(|temperature| temperature.to_string())
}

fn active_llm_extra_headers_json(root: &CredsRoot) -> Result<Option<String>> {
let headers = active_llm_extra_headers(root);
if headers.is_empty() {
Expand Down Expand Up @@ -310,6 +330,21 @@ fn parse_extra_headers_json(value: &str) -> Result<HashMap<String, String>> {
Ok(headers)
}

fn parse_llm_temperature(value: &str) -> Result<Option<f64>> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}
let temperature: f64 = trimmed.parse().context("temperature must be a number")?;
if !is_valid_llm_temperature(temperature) {
if !temperature.is_finite() {
anyhow::bail!("temperature must be finite");
}
anyhow::bail!("temperature must be between 0 and 2");
}
Ok(Some(temperature))
}

fn is_valid_header_name(name: &str) -> bool {
!name.is_empty()
&& name.bytes().all(|b| {
Expand Down Expand Up @@ -1370,6 +1405,25 @@ impl CredentialsVault {
active_llm_extra_headers_json(&load_credentials())
}

pub fn get_active_llm_temperature() -> Option<f32> {
let _guard = credentials_lock().lock();
active_llm_temperature(&load_credentials())
}

pub fn get_active_llm_temperature_string() -> Option<String> {
let _guard = credentials_lock().lock();
active_llm_temperature_string(&load_credentials())
}

pub fn set_active_llm_temperature(value: &str) -> Result<()> {
let _guard = credentials_lock().lock();
let temperature = parse_llm_temperature(value)?;
let mut root = load_credentials_for_update()?;
let entry = root.providers.llm.entry(root.active.llm.clone()).or_default();
entry.temperature = temperature;
save_credentials(&root)
}

pub fn set_active_llm_extra_headers_json(value: &str) -> Result<()> {
let _guard = credentials_lock().lock();
let headers = parse_extra_headers_json(value)?;
Expand Down Expand Up @@ -1406,7 +1460,7 @@ mod tests {
android_persistable_credentials, chunk_json_payload, credentials_cache,
get_android_marketplace_token_at, load_android_credentials_from_path,
load_android_credentials_from_path_with_crypto, load_android_credentials_into_cache_with,
lookup_marketplace_github_token, parse_extra_headers_json,
lookup_marketplace_github_token, parse_extra_headers_json, parse_llm_temperature,
reset_credentials_cache_for_tests, write_marketplace_github_token, CredsRoot,
MarketplaceGithubToken, KEYRING_CHUNK_MAX_UTF16_UNITS,
};
Expand Down Expand Up @@ -1716,4 +1770,52 @@ mod tests {
*credentials_cache().lock() = Some(CredsRoot::default());
let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn parse_llm_temperature_accepts_empty_and_valid_range() {
assert_eq!(parse_llm_temperature("").unwrap(), None);
assert_eq!(parse_llm_temperature(" 0.3 ").unwrap(), Some(0.3));
assert_eq!(parse_llm_temperature("2").unwrap(), Some(2.0));
}

#[test]
fn parse_llm_temperature_rejects_invalid_values() {
for value in ["abc", "-0.1", "2.1", "NaN", "inf"] {
assert!(
parse_llm_temperature(value).is_err(),
"{value} should be rejected"
);
}
}

#[test]
fn active_llm_temperature_ignores_invalid_persisted_values() {
for temperature in [-0.1, 2.5] {
let mut root = CredsRoot::default();
root.providers.llm.insert(
root.active.llm.clone(),
super::CredsLlmEntry {
temperature: Some(temperature),
..Default::default()
},
);

assert_eq!(super::active_llm_temperature(&root), None);
assert_eq!(super::active_llm_temperature_string(&root), None);
}

let mut root = CredsRoot::default();
root.providers.llm.insert(
root.active.llm.clone(),
super::CredsLlmEntry {
temperature: Some(0.7),
..Default::default()
},
);
assert_eq!(super::active_llm_temperature(&root), Some(0.7));
assert_eq!(
super::active_llm_temperature_string(&root).as_deref(),
Some("0.7")
);
}
}
Loading
Loading