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
227 changes: 220 additions & 7 deletions openless-all/app/crates/openless-core/src/api.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3438,7 +3438,40 @@ impl OpenLessBackend {
key: CredentialKey,
value: SecretValue,
) -> Result<CredentialsStatus, BackendError> {
if key.namespace == crate::CredentialNamespace::Llm
&& crate::llm_protocol::CONFIG_ACCOUNTS.contains(&key.account.as_str())
{
crate::llm_protocol::LlmProtocolConfig::default()
.apply(&key.account, value.expose_secret())?;
}
let invalidate = if key.namespace == crate::CredentialNamespace::Llm {
let id = match &key.provider_id {
Some(id) => id.clone(),
None => {
self.deps
.credential_store
.active_provider(crate::ProviderSlot::Llm)
.await?
}
};
self.list_channels(ChannelKind::Llm)
.await?
.into_iter()
.find(|channel| channel.id == id)
.map(|_| id)
} else {
None
};
self.deps.credential_store.write(key, value).await?;
if let Some(id) = invalidate {
self.deps
.credential_store
.mutate_channel(ChannelMutation::InvalidateTest {
kind: ChannelKind::Llm,
id,
})
.await?;
}
self.refresh_and_publish_credentials().await
}

Expand DownExpand Up@@ -3485,13 +3518,51 @@ impl OpenLessBackend {
id: String,
provider_type: String,
) -> Result<(), BackendError> {
self.apply_channel_mutation(ChannelMutation::SetProviderType {
kind,
id,
provider_type,
})
.await
.map(|_| ())
let provider_type = provider_type.trim().to_string();
if provider_type.trim().is_empty() {
return Err(BackendError::new(
BackendErrorCode::InvalidArgument,
"provider type must not be blank",
));
}
let previous = self
.list_channels(kind)
.await?
.into_iter()
.find(|channel| channel.id == id)
.ok_or_else(|| {
BackendError::new(BackendErrorCode::InvalidArgument, "unknown channel")
})?;
let key = CredentialKey::new(
crate::CredentialNamespace::Llm,
Some(id.clone()),
crate::llm_protocol::REQUEST_FORMAT_ACCOUNT,
)?;
let reset = kind == ChannelKind::Llm && previous.provider_type != provider_type;
let old_format = if reset {
let value = self.deps.credential_store.read(key.clone()).await?;
self.deps.credential_store.remove(key.clone()).await?;
value
} else {
None
};
let result = self
.deps
.credential_store
.mutate_channel(ChannelMutation::SetProviderType {
kind,
id,
provider_type,
})
.await
.map(|_| ());
if result.is_err() {
if let Some(value) = old_format {
self.deps.credential_store.write(key, value).await?;
}
}
result?;
self.refresh_and_publish_credentials().await.map(|_| ())
}

pub async fn delete_channel_if_blank(
Expand DownExpand Up@@ -3573,6 +3644,12 @@ impl OpenLessBackend {
.map(|_| ())
}

pub async fn invalidate_channel_tests(&self, kind: ChannelKind) -> Result<(), BackendError> {
self.apply_channel_mutation(ChannelMutation::InvalidateTests { kind })
.await
.map(|_| ())
}

pub async fn active_provider(&self, slot: ProviderSlot) -> Result<String, BackendError> {
self.deps.credential_store.active_provider(slot).await
}
Expand DownExpand Up@@ -8252,6 +8329,142 @@ mod tests {
));
}

#[tokio::test]
async fn invalidating_llm_tests_preserves_asr_test_results() {
let (backend, _) = backend();
for (kind, provider, name) in [
(ChannelKind::Llm, "custom", "first"),
(ChannelKind::Llm, "custom_messages", "second"),
(ChannelKind::Asr, "openai-compatible", "asr"),
] {
let id = backend
.create_channel(kind, provider.into(), name.into())
.await
.unwrap();
backend
.record_channel_test(kind, id, true, Some(1), None)
.await
.unwrap();
}

backend
.invalidate_channel_tests(ChannelKind::Llm)
.await
.unwrap();

assert!(backend
.list_channels(ChannelKind::Llm)
.await
.unwrap()
.iter()
.all(|channel| channel.last_test.is_none()));
assert!(backend.list_channels(ChannelKind::Asr).await.unwrap()[0]
.last_test
.is_some());
}

#[tokio::test]
async fn llm_protocol_mutations_reset_only_the_format_and_invalidate_tests() {
use crate::credentials::{CredentialNamespace, InMemoryCredentialStore, SecretValue};
use crate::llm_protocol::*;
let backend = OpenLessBackend::new(
BackendConfig {
data_dir: std::env::temp_dir()
.join(format!("openless-protocol-{}", uuid::Uuid::new_v4())),
..BackendConfig::default()
},
BackendDependencies {
host_actions: Arc::new(FakeHost::default()),
text_inserter: Arc::new(FakeInserter),
dictation_engine: Arc::new(FakeEngine),
task_spawner: Arc::new(TokioTaskSpawner),
credential_store: Arc::new(InMemoryCredentialStore::default()),
services: crate::domains::BackendServices::unsupported(),
local_asr_runtime: None,
marketplace_config: None,
selection_runtime: None,
selection_polisher: None,
qa_runtime: None,
},
)
.unwrap();
let id = backend
.create_channel(ChannelKind::Llm, "custom".into(), "test".into())
.await
.unwrap();
let key = |account: &str| {
CredentialKey::new(CredentialNamespace::Llm, Some(id.clone()), account).unwrap()
};
backend
.set_credential(key(REQUEST_FORMAT_ACCOUNT), SecretValue::new("messages"))
.await
.unwrap();
backend
.set_credential(
key(crate::credentials::LLM_API_KEY_ACCOUNT),
SecretValue::new("fixture-key"),
)
.await
.unwrap();
backend
.record_channel_test(ChannelKind::Llm, id.clone(), true, Some(1), None)
.await
.unwrap();
assert!(backend.list_channels(ChannelKind::Llm).await.unwrap()[0]
.last_test
.is_some());
backend
.set_credential(
key(crate::credentials::LLM_MODEL_ACCOUNT),
SecretValue::new("new-model"),
)
.await
.unwrap();
assert!(backend.list_channels(ChannelKind::Llm).await.unwrap()[0]
.last_test
.is_none());
assert_eq!(
backend
.read_credential(key(REQUEST_FORMAT_ACCOUNT))
.await
.unwrap()
.unwrap()
.expose_secret(),
"messages"
);
assert!(backend
.set_credential(key(REQUEST_FORMAT_ACCOUNT), SecretValue::new("invalid"))
.await
.is_err());
backend
.set_channel_provider_type(ChannelKind::Llm, id.clone(), "custom_responses".into())
.await
.unwrap();
assert!(backend
.read_credential(key(REQUEST_FORMAT_ACCOUNT))
.await
.unwrap()
.is_none());
assert_eq!(
backend
.read_credential(key(crate::credentials::LLM_API_KEY_ACCOUNT))
.await
.unwrap()
.unwrap()
.expose_secret(),
"fixture-key"
);
assert_eq!(
backend
.read_credential(key(crate::credentials::LLM_MODEL_ACCOUNT))
.await
.unwrap()
.unwrap()
.expose_secret(),
"new-model"
);
}

#[tokio::test]
async fn lifecycle_is_idempotent_and_emits_started_once_per_transition() {
let (backend, _) = backend();
Expand Down
14 changes: 7 additions & 7 deletions openless-all/app/crates/openless-core/src/cloud_providers.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,8 @@ pub const SHARED_CLOUD_LLM_PROVIDER_TYPES: &[&str] = &[
"minimax",
"stepfun",
"custom",
"custom_responses",
"custom_messages",
];

pub const SHARED_OMNI_PROVIDER_TYPES: &[&str] = &["openai", "gemini", "dashscope-omni", "custom"];
Expand DownExpand Up@@ -1037,12 +1039,9 @@ async fn build_cloud_polisher_provider(
return Ok(CloudPolisherProvider::Gemini(provider));
}

let base_url = endpoint
.trim()
.trim_end_matches('/')
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();
let protocol =
crate::llm_protocol::LlmProtocolConfig::load(credentials, channel_id, provider_type)
.await?;
let temperature = read_channel_credential(
credentials,
CredentialNamespace::Llm,
Expand All@@ -1068,10 +1067,11 @@ async fn build_cloud_polisher_provider(
let config = crate::polish::OpenAICompatibleConfig::new(
provider_type,
"OpenLess LLM",
base_url,
endpoint,
api_key,
model,
)
.with_protocol(protocol)
.with_thinking_enabled(context.polish.llm_thinking_enabled)
.with_temperature(crate::polish::openai_compatible_temperature_for_provider(
provider_type,
Expand Down
19 changes: 19 additions & 0 deletions openless-all/app/crates/openless-core/src/credentials.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ pub struct ChannelTestSummary {

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChannelMutation {
InvalidateTest {
kind: ChannelKind,
id: String,
},
InvalidateTests {
kind: ChannelKind,
},
/// Commit a prepared local runtime and its channel in one metadata revision.
ActivateLocalAsr {
id: Option<String>,
Expand DownExpand Up@@ -519,6 +526,8 @@ impl CredentialMetadata {
ChannelMutation::ActivateLocalAsr { .. } => ChannelKind::Asr,
ChannelMutation::Create { kind, .. }
| ChannelMutation::SetProviderType { kind, .. }
| ChannelMutation::InvalidateTest { kind, .. }
| ChannelMutation::InvalidateTests { kind }
| ChannelMutation::DeleteIfBlank { kind, .. }
| ChannelMutation::Rename { kind, .. }
| ChannelMutation::Delete { kind, .. }
Expand DownExpand Up@@ -630,6 +639,16 @@ impl CredentialMetadata {
channel.last_test = None;
(kind, ChannelMutationResult::Applied)
}
ChannelMutation::InvalidateTest { kind, id } => {
find_channel_mut(&mut self.channels, kind, &id)?.last_test = None;
(kind, ChannelMutationResult::Applied)
}
ChannelMutation::InvalidateTests { kind } => {
for channel in self.channels.entry(kind).or_default() {
channel.last_test = None;
}
(kind, ChannelMutationResult::Applied)
}
ChannelMutation::DeleteIfBlank { kind, id } => {
let channels = self.channels.entry(kind).or_default();
let before = channels.len();
Expand Down
21 changes: 21 additions & 0 deletions openless-all/app/crates/openless-core/src/credentials_legacy.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,10 @@ struct LegacyEntry {
xfyun_api_key: Option<String>,
temperature: Option<f64>,
extra_headers: Option<BTreeMap<String, String>>,
request_format: Option<String>,
messages_thinking: Option<String>,
max_tokens: Option<String>,
thinking_budget: Option<String>,
}

impl Default for LegacyEntry {
Expand All@@ -125,6 +129,10 @@ impl Default for LegacyEntry {
xfyun_api_key: None,
temperature: None,
extra_headers: None,
request_format: None,
messages_thinking: None,
max_tokens: None,
thinking_budget: None,
}
}
}
Expand All@@ -145,6 +153,10 @@ impl LegacyEntry {
&self.advanced_config,
&self.xfyun_app_id,
&self.xfyun_api_key,
&self.request_format,
&self.messages_thinking,
&self.max_tokens,
&self.thinking_budget,
]
.into_iter()
.any(|value| value.as_deref().is_some_and(|value| !value.is_empty()))
Expand DownExpand Up@@ -347,6 +359,15 @@ fn decode_entry(
(endpoint, entry.base_url),
(model, entry.model),
];
if namespace == CredentialNamespace::Llm {
use crate::llm_protocol::*;
fields.extend([
(REQUEST_FORMAT_ACCOUNT, entry.request_format),
(MESSAGES_THINKING_ACCOUNT, entry.messages_thinking),
(MAX_TOKENS_ACCOUNT, entry.max_tokens),
(THINKING_BUDGET_ACCOUNT, entry.thinking_budget),
]);
}
if namespace == CredentialNamespace::Asr {
fields.extend([
(VOLCENGINE_APP_KEY_ACCOUNT, entry.app_key),
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/crates/openless-core/src/domains.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,8 @@ pub enum ProviderKind {
#[serde(rename_all = "camelCase")]
pub struct ProviderRequest {
pub kind: ProviderKind,
#[serde(default)]
pub thinking_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_id: Option<String>,
}
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/crates/openless-core/src/lib.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@ pub mod host_document;
mod hotkey_interpreter;
mod less_computer;
pub mod llm_gemini;
pub mod llm_protocol;
mod marketplace;
pub mod model_store;
pub mod net;
Expand Down
Loading
Loading