Compare commits

...
Author SHA1 Message Date
Brandon Thomas 16f93cba52 route requests based on configured service 2025-06-27 21:10:23 -04:00
Brandon Thomas 045aefcd13 set and get provider priority 2025-06-27 20:52:20 -04:00
Brandon Thomas 5f6bee5a87 update priority 2025-06-27 20:32:45 -04:00
Brandon Thomas 068ff6ac10 get/set provider priority 2025-06-27 19:47:44 -04:00
Brandon Thomas 2b5bedf131 provider preference model 2025-06-27 18:43:47 -04:00
22 changed files with 484 additions and 52 deletions
@@ -2,15 +2,20 @@ use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_a
use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
@@ -59,10 +64,6 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -85,6 +86,8 @@ impl SerializeMarker for EnqueueTextToImageSuccessResponse {}
pub enum EnqueueTextToImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for image generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
@@ -102,6 +105,7 @@ pub async fn enqueue_text_to_image_command(
request: EnqueueTextToImageRequest,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
provider_priority_store: State<'_, ProviderPriorityStore>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
@@ -115,6 +119,7 @@ pub async fn enqueue_text_to_image_command(
&app,
request,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&app_env_configs,
@@ -137,6 +142,11 @@ pub async fn enqueue_text_to_image_command(
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalImageError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
error_message = "No configured provider available for image generation";
}
InternalImageError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
@@ -173,6 +183,7 @@ pub async fn handle_request(
app: &AppHandle,
request: EnqueueTextToImageRequest,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
app_env_configs: &AppEnvConfigs,
@@ -197,9 +208,22 @@ pub async fn handle_request(
}
};
if fal_creds_manager.has_apparent_api_token()? {
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await
} else {
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await
let priority = provider_priority_store.get_priority()?;
for provider in priority.iter() {
match provider {
Provider::Sora => {} // Fallthrough
Provider::Artcraft => {
return handle_image_artcraft(
request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await;
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await;
}
}
}
}
Err(InternalImageError::NoProviderAvailable)
}
@@ -8,6 +8,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
@@ -56,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageErrorType;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -90,6 +93,8 @@ impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {}
pub enum EnqueueImageTo3dObjectErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for object generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
@@ -105,6 +110,7 @@ pub async fn enqueue_image_to_3d_object_command(
app: AppHandle,
request: EnqueueImageTo3dObjectRequest,
app_data_root: State<'_, AppDataRoot>,
provider_priority_store: State<'_, ProviderPriorityStore>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -117,6 +123,7 @@ pub async fn enqueue_image_to_3d_object_command(
&app,
request,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
@@ -134,7 +141,12 @@ pub async fn enqueue_image_to_3d_object_command(
InternalObjectError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
error_message = "No model specified for object generation";
}
InternalObjectError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageTo3dObjectErrorType::NoProviderAvailable;
error_message = "No configured provider available for object generation";
}
InternalObjectError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
@@ -167,16 +179,29 @@ pub async fn handle_request(
app: &AppHandle,
request: EnqueueImageTo3dObjectRequest,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalObjectError> {
if fal_creds_manager.has_apparent_api_token()? {
handle_object_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_object_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
let priority = provider_priority_store.get_priority()?;
for provider in priority.iter() {
match provider {
Provider::Sora => {} // Fallthrough
Provider::Artcraft => {
return Ok(handle_object_artcraft(
request, &app, app_data_root, storyteller_creds_manager).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_object_fal(
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
}
}
}
}
Ok(())
Err(InternalObjectError::NoProviderAvailable)
}
@@ -7,6 +7,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalObjectError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
@@ -4,7 +4,6 @@ use crate::core::commands::enqueue::video::internal_video_error::InternalVideoEr
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
@@ -57,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectErrorType;
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
#[derive(Deserialize)]
pub struct EnqueueImageToVideoRequest {
@@ -79,6 +81,8 @@ impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
pub enum EnqueueImageToVideoErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// No model available for video generation
NoProviderAvailable,
/// Generic server error
ServerError,
/// No Fal API key available
@@ -94,6 +98,7 @@ pub async fn enqueue_image_to_video_command(
app: AppHandle,
request: EnqueueImageToVideoRequest,
app_data_root: State<'_, AppDataRoot>,
provider_priority_store: State<'_, ProviderPriorityStore>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -106,6 +111,7 @@ pub async fn enqueue_image_to_video_command(
&app,
request,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
@@ -123,7 +129,12 @@ pub async fn enqueue_image_to_video_command(
InternalVideoError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
error_message = "No model specified for video generation";
}
InternalVideoError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
error_message = "No configured provider available for video generation";
}
InternalVideoError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
@@ -156,16 +167,29 @@ pub async fn handle_request(
app: &AppHandle,
request: EnqueueImageToVideoRequest,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
if fal_creds_manager.has_apparent_api_token()? {
handle_video_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_video_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
let priority = provider_priority_store.get_priority()?;
for provider in priority.iter() {
match provider {
Provider::Sora => {} // Fallthrough
Provider::Artcraft => {
return Ok(handle_video_artcraft(
request, &app, app_data_root, storyteller_creds_manager).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_video_fal(
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
}
}
}
}
Ok(())
Err(InternalVideoError::NoProviderAvailable)
}
@@ -7,6 +7,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalVideoError {
NoModelSpecified,
NoProviderAvailable,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
@@ -3,4 +3,5 @@ pub mod enqueue;
pub mod flip_image;
pub mod get_app_info_command;
pub mod platform_info_command;
pub mod providers;
pub mod response;
@@ -0,0 +1,33 @@
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use errors::AnyhowResult;
use log::{error, info};
use serde_derive::Serialize;
use tauri::{AppHandle, State};
#[derive(Serialize)]
pub struct GetProviderOrderResponse {
pub providers: Vec<Provider>,
}
impl SerializeMarker for GetProviderOrderResponse{}
#[tauri::command]
pub async fn get_provider_order_command(
provider_priority_store: State<'_, ProviderPriorityStore>,
) -> ResponseOrErrorMessage<GetProviderOrderResponse> {
info!("get_provider_order_command called");
let providers = provider_priority_store.get_priority()
.map_err(|err| {
error!("Failed to get provider order: {:?}", err);
"Failed to retrieve provider order"
})?;
Ok(GetProviderOrderResponse {
providers,
}.into())
}
@@ -0,0 +1,2 @@
pub mod get_provider_order_command;
pub mod set_provider_order_command;
@@ -0,0 +1,39 @@
use crate::core::commands::response::shorthand::{ResponseOrErrorMessage, SuccessOrErrorMessage};
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use errors::AnyhowResult;
use log::{error, info};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
#[derive(Deserialize)]
pub struct SetProviderOrderRequest {
pub providers: Vec<Provider>,
}
#[tauri::command]
pub async fn set_provider_order_command(
request: SetProviderOrderRequest,
provider_priority_store: State<'_, ProviderPriorityStore>,
app_data_root: State<'_, AppDataRoot>,
) -> SuccessOrErrorMessage {
info!("set_provider_order_command called");
provider_priority_store.set_priority(&request.providers)
.map_err(|err| {
error!("Failed to set provider order: {:?}", err);
"Failed to set provider order"
})?;
provider_priority_store.persist_to_filesystem(&app_data_root)
.map_err(|err| {
error!("Failed to persist provider order to filesystem: {:?}", err);
"Failed to persist provider order"
})?;
Ok(().into())
}
@@ -30,3 +30,19 @@ pub type Response<SuccessPayload, ErrType, ErrPayload> =
/// No inner payloads for this type. Just strings as messages.
pub type SimpleResponse = Response<(), (), ()>;
/// Either an empty success payload or error message.
pub type SuccessOrErrorMessage =
Result<CommandSuccessResponseWrapper<()>, CommandErrorResponseWrapper<(), ()>>;
/// Either a success or error message.
pub type ResponseOrErrorMessage<SuccessPayload> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ()>>;
/// Either a success or error type.
pub type ResponseOrErrorType<SuccessPayload, ErrType> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ()>>;
/// Either a success or error payload.
pub type ResponseOrError<SuccessPayload, ErrPayload> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ErrPayload>>;
@@ -36,6 +36,10 @@ impl AppSettingsDir {
self.path.join("env_configs.json")
}
pub fn get_provider_preferences_path(&self) -> PathBuf {
self.path.join("provider_preferences.json")
}
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
let serializable = prefs.to_serializable();
@@ -5,4 +5,5 @@ pub mod data_dir;
pub mod expanduser;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
pub mod os_platform;
pub mod provider_priority;
@@ -0,0 +1,162 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::{Arc, RwLock};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provider {
Artcraft,
Fal,
Sora,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ProviderPriority {
/// Providers may occur zero or one time.
/// They are ordered by priority, highest to lowest.
provider_priority: Vec<Provider>,
}
#[derive(Clone)]
pub struct ProviderPriorityStore {
provider_priority: Arc<RwLock<ProviderPriority>>,
}
impl ProviderPriority {
pub fn default() -> Self {
Self {
provider_priority: vec![
Provider::Artcraft,
Provider::Sora,
Provider::Fal,
],
}
}
pub fn set_priority(&mut self, ordered_list: &[Provider]) -> AnyhowResult<()> {
let set : HashSet<&Provider> = HashSet::from_iter(ordered_list.iter());
if set.len() != ordered_list.len() {
return Err(anyhow::anyhow!("Duplicate providers in the ordered list"));
}
self.provider_priority = ordered_list.to_vec();
Ok(())
}
pub fn get_priority(&self) -> &[Provider] {
&self.provider_priority
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.settings_dir().get_provider_preferences_path();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let providers : Self = serde_json::from_str(&contents)?;
Ok(Some(providers))
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.settings_dir().get_provider_preferences_path();
let json = serde_json::to_string(self)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(json.as_bytes())?;
file.flush()?;
Ok(())
}
}
impl ProviderPriorityStore {
pub fn default() -> Self {
Self {
provider_priority: Arc::new(RwLock::new(ProviderPriority::default())),
}
}
pub fn set_priority(&self, ordered_list: &[Provider]) -> AnyhowResult<()> {
match self.provider_priority.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock on provider priority: {}", err))
}
Ok(mut providers) => {
providers.set_priority(ordered_list)?;
Ok(())
}
}
}
pub fn get_priority(&self) -> AnyhowResult<Vec<Provider>> {
match self.provider_priority.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
}
Ok(providers) => {
Ok(providers.get_priority().to_vec())
}
}
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
match ProviderPriority::from_filesystem_configs(app_data_root) {
Err(err) => {
Err(anyhow::anyhow!("Failed to read provider preferences from filesystem: {}", err))
}
Ok(None) => Ok(None),
Ok(Some(providers)) => {
Ok(Some(Self {
provider_priority: Arc::new(RwLock::new(providers)),
}))
}
}
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
match self.provider_priority.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
}
Ok(providers) => {
providers.persist_to_filesystem(app_data_root)?;
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_priority() {
let priority = ProviderPriority::default();
assert_eq!(priority.get_priority(), &[Provider::Artcraft, Provider::Sora, Provider::Fal]);
}
#[test]
fn test_set_priority() -> AnyhowResult<()> {
let mut priority = ProviderPriority::default();
priority.set_priority(&[Provider::Fal, Provider::Artcraft])?;
assert_eq!(priority.get_priority(), &[Provider::Fal, Provider::Artcraft]);
Ok(())
}
#[test]
fn test_set_priority_with_duplicates() {
let mut priority = ProviderPriority::default();
let result = priority.set_priority(&[Provider::Artcraft, Provider::Artcraft]);
assert!(result.is_err());
}
}
+21
View File
@@ -11,11 +11,14 @@ use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enque
use crate::core::commands::flip_image::flip_image;
use crate::core::commands::get_app_info_command::get_app_info_command;
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::state::provider_priority::ProviderPriorityStore;
use crate::core::threads::discord_presence_thread::discord_presence_thread;
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::core::utils::webview_unsafe::webview_unsafe_for_app;
@@ -77,6 +80,21 @@ pub fn run() {
let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root)
.expect("AppEnvConfigs should be loaded from disk");
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(&app_data_root) {
Ok(Some(priority)) => {
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
priority
}
Ok(None) => {
println!("No provider priority found on disk, using default.");
ProviderPriorityStore::default()
}
Err(err) => {
eprintln!("Failed to read provider priority from disk: {:?}", err);
ProviderPriorityStore::default()
}
};
println!("Initializing backend runtime...");
@@ -165,6 +183,7 @@ pub fn run() {
.manage(app_preferences)
.manage(fal_creds_manager)
.manage(fal_task_queue)
.manage(provider_priority)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.manage(storyteller_creds_manager_3)
@@ -180,6 +199,8 @@ pub fn run() {
get_app_info_command,
get_app_preferences_command,
get_fal_api_key_command,
get_provider_order_command,
set_provider_order_command,
open_sora_login_command,
platform_info_command,
set_fal_api_key_command,
@@ -1,5 +1,5 @@
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
@@ -42,6 +42,9 @@ impl FalCredentialManager {
}
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
if key.0.trim().is_empty() {
return Err(anyhow!("FAL API key cannot be empty")); // TODO: Also handle upstream.
}
self.key.set_clone(key)
}
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import {
DndContext,
closestCenter,
@@ -21,9 +21,10 @@ import {
faGripVertical,
faSpinnerThird,
} from "@fortawesome/pro-solid-svg-icons";
import { SetProviderOrder, Provider, GetProviderOrder } from "@storyteller/tauri-api";
interface RouterItem {
id: string;
interface ProviderItem {
id: Provider;
name: string;
emoji: string;
}
@@ -81,16 +82,40 @@ const SortableItem = ({
);
};
export const RouterPrioritySettingsPane = () => {
const [items, setItems] = useState<RouterItem[]>([
{ id: "artcraft-3d", name: "ArtCraft 3D", emoji: "🎨" },
{ id: "gpt-image-1", name: "GPT Image 1 (GPT-4o)", emoji: "🤖" },
{ id: "flux-pro-ultra", name: "Flux Pro Ultra", emoji: "⚡" },
{ id: "recraft-3", name: "Recraft 3", emoji: "🌟" },
{ id: "flux-kontext", name: "Flux.1 Kontext", emoji: "🔮" },
]);
const ProviderItemMap = {
[Provider.ArtCraft]: { id: Provider.ArtCraft, name: "ArtCraft", emoji: "🎨" },
[Provider.Fal]: { id: Provider.Fal, name: "Fal", emoji: "🤖" },
[Provider.Sora]: { id: Provider.Sora, name: "Sora / ChatGPT", emoji: "⚡" },
};
export const ProviderPrioritySettingsPane = () => {
const [items, setItems] = useState<ProviderItem[]>([]);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
const fetchData = async () => {
const providers = await GetProviderOrder();
let items: ProviderItem[] = [];
// Add providers from backend (in order)
for (let provider of providers.payload.providers) {
items.push(ProviderItemMap[provider]);
}
// Add providers not in backend (in order)
for (const [key, value] of Object.entries(ProviderItemMap)) {
if (!providers.payload.providers.includes(key as Provider)) {
items.push(value)
}
}
setItems(items);
};
fetchData();
}, []);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@@ -98,19 +123,19 @@ export const RouterPrioritySettingsPane = () => {
})
);
const updateRouterPriorityOnBackend = async (newOrder: RouterItem[]) => {
const updateProviderPriorityOnBackend = async (newOrder: ProviderItem[]) => {
try {
setIsUpdating(true);
console.log("Updating router priority on backend:", newOrder);
console.log("Updating provider priority on backend:", newOrder);
// Simulate API call delay
await new Promise((resolve) => setTimeout(resolve, 500)); //remove this - BFlat
let ordering = newOrder.map((item) => item.id);
console.log("Provider order:", ordering);
//api call here
await SetProviderOrder({ providers: ordering });
console.log("Router priority updated successfully");
console.log("Provider priority updated successfully");
} catch (error) {
console.error("Failed to update router priority:", error);
console.error("Failed to update provider priority:", error);
} finally {
setIsUpdating(false);
}
@@ -127,7 +152,7 @@ export const RouterPrioritySettingsPane = () => {
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
// Send update to backend
updateRouterPriorityOnBackend(newOrder);
updateProviderPriorityOnBackend(newOrder);
return newOrder;
});
@@ -138,8 +163,8 @@ export const RouterPrioritySettingsPane = () => {
<div className="space-y-4">
<div>
<p className="text-sm text-white/70 mb-4">
Drag and drop to reorder model priority. Higher items will be tried
first.
Drag and drop to reorder model provider priority. Higher items will be tried
first. You can use this to control favorite services and spending.
</p>
</div>
@@ -13,7 +13,7 @@ import { MiscSettingsPane } from "./panes/MiscSettingsPane";
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
import { RouterPrioritySettingsPane } from "./panes/RouterPrioritySettingsPane";
import { ProviderPrioritySettingsPane } from "./panes/ProviderPrioritySettingsPane";
interface SettingsModalProps {
isOpen: boolean;
@@ -27,7 +27,7 @@ type SettingsSection =
| "accounts"
| "alerts"
| "about"
| "router_priority";
| "provider_priority";
export const SettingsModal = ({
isOpen,
@@ -41,13 +41,13 @@ export const SettingsModal = ({
const sections = [
{ id: "general" as const, label: "General", icon: faCog },
{ id: "accounts" as const, label: "Accounts", icon: faUser },
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
{ id: "about" as const, label: "About", icon: faCircleInfo },
{
id: "router_priority" as const,
label: "Model Router Priority",
id: "provider_priority" as const,
label: "Provider Priority",
icon: faRoute,
},
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
{ id: "about" as const, label: "About", icon: faCircleInfo },
//{ id: "video" as const, label: "Video", icon: faVideo },
//{ id: "image" as const, label: "Image", icon: faImage },
];
@@ -66,8 +66,8 @@ export const SettingsModal = ({
);
case "about":
return <AboutSettingsPane />;
case "router_priority":
return <RouterPrioritySettingsPane />;
case "provider_priority":
return <ProviderPrioritySettingsPane />;
}
};
+4
View File
@@ -17,3 +17,7 @@ export * from "./lib/sora/SoraImageRemix.js";
export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/sora/waitForSoraLogin.js";
export * from "./lib/system/GetAppInfo.js";
export * from "./lib/providers/SetProviderOrder.js";
export * from "./lib/providers/Provider.js";
export * from "./lib/providers/GetProviderOrder.js";
@@ -0,0 +1,22 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { Provider } from "./Provider";
export interface GetProviderOrderSuccess extends CommandResult {
payload: GetProviderOrderPayload;
}
export interface GetProviderOrderPayload {
providers: Provider[];
}
// Returns the Success and Error variants directly.
// Throws on Network/Tauri errors.
export const GetProviderOrder = async () : Promise<GetProviderOrderSuccess> => {
try {
return await invoke("get_provider_order_command") as GetProviderOrderSuccess;
} catch (error) {
// NB: Endpoint should be infalliable
throw error;
}
}
@@ -0,0 +1,6 @@
export enum Provider {
ArtCraft = "artcraft",
Fal = "fal",
Sora = "sora",
}
@@ -0,0 +1,17 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { Provider } from "./Provider";
export interface SetProviderOrderRequest {
providers: Provider[];
}
export const SetProviderOrder = async (request: SetProviderOrderRequest) : Promise<CommandResult> => {
const result = await invoke("set_provider_order_command", {
request: {
providers: request.providers,
}
});
return (result as CommandResult);
}