Bump version to 2.0.0 and upload install-galaxy.sh in deploy script
- Update version from 1.6.3 to 2.0.0 in app/Cargo.toml and Cargo.lock - Add install-galaxy.sh upload step to build-and-deploy-hermes script - Include pending AI provider and agent changes
This commit is contained in:
Generated
+1
-1
@@ -5653,7 +5653,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "galaxy"
|
name = "galaxy"
|
||||||
version = "1.6.3"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr",
|
"addr",
|
||||||
"aha-reqwest-eventsource",
|
"aha-reqwest-eventsource",
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ description = "Galaxy - AI-powered terminal"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
autobins = false
|
autobins = false
|
||||||
name = "galaxy"
|
name = "galaxy"
|
||||||
version = "1.6.3"
|
version = "2.0.0"
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -2250,7 +2250,7 @@ impl AIConversation {
|
|||||||
|
|
||||||
if let Some(usage_metadata) = usage_metadata {
|
if let Some(usage_metadata) = usage_metadata {
|
||||||
self.conversation_usage_metadata.context_window_usage =
|
self.conversation_usage_metadata.context_window_usage =
|
||||||
usage_metadata.context_window_usage;
|
usage_metadata.context_window_usage.clamp(0.0, 1.0);
|
||||||
self.conversation_usage_metadata.credits_spent = usage_metadata.credits_spent;
|
self.conversation_usage_metadata.credits_spent = usage_metadata.credits_spent;
|
||||||
self.conversation_usage_metadata.platform_credits_spent =
|
self.conversation_usage_metadata.platform_credits_spent =
|
||||||
usage_metadata.platform_credits_spent;
|
usage_metadata.platform_credits_spent;
|
||||||
|
|||||||
@@ -678,7 +678,8 @@ pub fn build_stream_finished(
|
|||||||
/ max_context_tokens as f32
|
/ max_context_tokens as f32
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
}
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ pub async fn execute(
|
|||||||
messages.extend(new_input_messages);
|
messages.extend(new_input_messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for message in &mut messages {
|
||||||
|
message.truncate_tool_results_for_provider_request();
|
||||||
|
}
|
||||||
|
|
||||||
request_translator::sanitize_messages_for_bedrock(&mut messages);
|
request_translator::sanitize_messages_for_bedrock(&mut messages);
|
||||||
|
|
||||||
let system_prompt = request_translator::extract_system_prompt(request);
|
let system_prompt = request_translator::extract_system_prompt(request);
|
||||||
|
|||||||
@@ -1942,7 +1942,7 @@ impl AgentInputFooter {
|
|||||||
if let Some(conversation) =
|
if let Some(conversation) =
|
||||||
BlocklistAIHistoryModel::as_ref(ctx).active_conversation(self.terminal_view_id)
|
BlocklistAIHistoryModel::as_ref(ctx).active_conversation(self.terminal_view_id)
|
||||||
{
|
{
|
||||||
let usage = conversation.context_window_usage();
|
let usage = conversation.context_window_usage().clamp(0.0, 1.0);
|
||||||
let icon = icon_for_context_window_usage(usage);
|
let icon = icon_for_context_window_usage(usage);
|
||||||
let remaining_pct = ((1.0 - usage) * 100.0).round() as i32;
|
let remaining_pct = ((1.0 - usage) * 100.0).round() as i32;
|
||||||
|
|
||||||
|
|||||||
@@ -3139,17 +3139,20 @@ impl BlocklistAIController {
|
|||||||
// Check if this error is eligible for corrective retry.
|
// Check if this error is eligible for corrective retry.
|
||||||
// Similar to loop detection, inject a message telling the LLM
|
// Similar to loop detection, inject a message telling the LLM
|
||||||
// to try a different approach rather than just failing.
|
// to try a different approach rather than just failing.
|
||||||
|
// Exclude errors that are proxy/config issues (cache_control,
|
||||||
|
// BadRequestError from LiteLLM) since the LLM can't fix those.
|
||||||
let error_str = format!("{e}");
|
let error_str = format!("{e}");
|
||||||
let is_corrective_retry_candidate = !matches!(
|
let is_proxy_config_error = error_str.contains("cache_control")
|
||||||
e.as_ref(),
|
|| error_str.contains("tool_use` ids were found without")
|
||||||
AIApiError::QuotaLimit { .. }
|
|| error_str.contains("BadRequestError");
|
||||||
) && (error_str.contains("ValidationException")
|
let is_corrective_retry_candidate = !is_proxy_config_error
|
||||||
|| error_str.contains("validation")
|
&& !matches!(e.as_ref(), AIApiError::QuotaLimit { .. })
|
||||||
|| error_str.contains("context window")
|
&& (error_str.contains("ValidationException")
|
||||||
|| error_str.contains("too many tokens")
|
|| error_str.contains("context window")
|
||||||
|| error_str.contains("input is too long")
|
|| error_str.contains("too many tokens")
|
||||||
|| error_str.contains("throttl")
|
|| error_str.contains("input is too long")
|
||||||
|| error_str.contains("ThrottlingException"));
|
|| error_str.contains("throttl")
|
||||||
|
|| error_str.contains("ThrottlingException"));
|
||||||
|
|
||||||
const MAX_ERROR_RETRIES: usize = 2;
|
const MAX_ERROR_RETRIES: usize = 2;
|
||||||
let retry_count = self
|
let retry_count = self
|
||||||
@@ -4026,7 +4029,7 @@ impl BlocklistAIController {
|
|||||||
let max_ctx = context_window_for_model(&active_model_id);
|
let max_ctx = context_window_for_model(&active_model_id);
|
||||||
let new_usage =
|
let new_usage =
|
||||||
(summary_tokens + remaining_msgs_tokens) as f32 / max_ctx as f32;
|
(summary_tokens + remaining_msgs_tokens) as f32 / max_ctx as f32;
|
||||||
conversation.set_context_window_usage(new_usage);
|
conversation.set_context_window_usage(new_usage.clamp(0.0, 1.0));
|
||||||
conversation
|
conversation
|
||||||
.set_current_context_tokens(summary_tokens + remaining_msgs_tokens);
|
.set_current_context_tokens(summary_tokens + remaining_msgs_tokens);
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ impl ResponseStream {
|
|||||||
base_url: client_config.base_url.clone(),
|
base_url: client_config.base_url.clone(),
|
||||||
api_key: client_config.api_key.clone(),
|
api_key: client_config.api_key.clone(),
|
||||||
model: Some(model_id.to_string()),
|
model: Some(model_id.to_string()),
|
||||||
|
max_input_tokens: client_config.max_input_tokens,
|
||||||
|
max_output_tokens: client_config.max_output_tokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -522,7 +522,8 @@ impl ConversationUsageView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
labels.push(render_label_text("Context window used", appearance));
|
labels.push(render_label_text("Context window used", appearance));
|
||||||
let context_usage_pct = self.usage_info.context_window_usage * 100.;
|
let context_window_usage = self.usage_info.context_window_usage.clamp(0.0, 1.0);
|
||||||
|
let context_usage_pct = context_window_usage * 100.;
|
||||||
let context_usage_str = if context_window_breakdown_enabled && self.context_window_expanded
|
let context_usage_str = if context_window_breakdown_enabled && self.context_window_expanded
|
||||||
{
|
{
|
||||||
format!("{context_usage_pct:.2}%")
|
format!("{context_usage_pct:.2}%")
|
||||||
@@ -540,7 +541,7 @@ impl ConversationUsageView {
|
|||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
ConstrainedBox::new(render_context_window_usage_icon(
|
ConstrainedBox::new(render_context_window_usage_icon(
|
||||||
self.usage_info.context_window_usage,
|
context_window_usage,
|
||||||
theme,
|
theme,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
|
|||||||
+100
-25
@@ -602,7 +602,7 @@ pub struct LLMPreferences {
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
openai_provider_routing: HashMap<String, super::openai::client::OpenAIClientConfig>,
|
openai_provider_routing: HashMap<String, super::openai::client::OpenAIClientConfig>,
|
||||||
/// Models fetched from the OpenAI-compatible /models endpoint at runtime.
|
/// Models fetched from the OpenAI-compatible /models endpoint at runtime.
|
||||||
/// Stored in memory only — not persisted to TOML.
|
/// Used as a short-lived fallback while the fetched list is persisted to settings.
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
fetched_openai_models: Vec<OpenAIModelConfig>,
|
fetched_openai_models: Vec<OpenAIModelConfig>,
|
||||||
}
|
}
|
||||||
@@ -670,10 +670,18 @@ impl LLMPreferences {
|
|||||||
AISettingsChangedEvent::BedrockEnabled { .. }
|
AISettingsChangedEvent::BedrockEnabled { .. }
|
||||||
| AISettingsChangedEvent::OpenAIEnabled { .. }
|
| AISettingsChangedEvent::OpenAIEnabled { .. }
|
||||||
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
|
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
|
||||||
|
| AISettingsChangedEvent::OpenAIApiKey { .. }
|
||||||
|
| AISettingsChangedEvent::OpenAIModels { .. }
|
||||||
|
| AISettingsChangedEvent::OpenAIProviders { .. }
|
||||||
) {
|
) {
|
||||||
me.inject_bedrock_models(ctx);
|
me.inject_bedrock_models(ctx);
|
||||||
me.inject_openai_models(ctx);
|
me.inject_openai_models(ctx);
|
||||||
if matches!(event, AISettingsChangedEvent::OpenAIEnabled { .. } | AISettingsChangedEvent::OpenAIBaseUrl { .. }) {
|
if matches!(
|
||||||
|
event,
|
||||||
|
AISettingsChangedEvent::OpenAIEnabled { .. }
|
||||||
|
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
|
||||||
|
| AISettingsChangedEvent::OpenAIApiKey { .. }
|
||||||
|
) {
|
||||||
me.fetch_openai_models_from_endpoint(ctx);
|
me.fetch_openai_models_from_endpoint(ctx);
|
||||||
}
|
}
|
||||||
// Safety: ensure the default model is still present in choices.
|
// Safety: ensure the default model is still present in choices.
|
||||||
@@ -961,11 +969,17 @@ impl LLMPreferences {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Models come exclusively from the in-memory /models endpoint fetch.
|
|
||||||
let mut provider_entries: Vec<(String, String, Option<String>, Vec<OpenAIModelConfig>)> =
|
let mut provider_entries: Vec<(String, String, Option<String>, Vec<OpenAIModelConfig>)> =
|
||||||
Vec::new();
|
Vec::new();
|
||||||
|
|
||||||
if !self.fetched_openai_models.is_empty() {
|
let configured_models = settings.openai_models.value().clone();
|
||||||
|
let single_provider_models = if configured_models.is_empty() {
|
||||||
|
self.fetched_openai_models.clone()
|
||||||
|
} else {
|
||||||
|
configured_models
|
||||||
|
};
|
||||||
|
|
||||||
|
if !single_provider_models.is_empty() {
|
||||||
let base_url = settings.openai_base_url.value().clone();
|
let base_url = settings.openai_base_url.value().clone();
|
||||||
let api_key = {
|
let api_key = {
|
||||||
let key = settings.openai_api_key.value().clone();
|
let key = settings.openai_api_key.value().clone();
|
||||||
@@ -980,9 +994,27 @@ impl LLMPreferences {
|
|||||||
} else {
|
} else {
|
||||||
"LiteLLM".to_string()
|
"LiteLLM".to_string()
|
||||||
};
|
};
|
||||||
provider_entries.push((name, base_url, api_key, self.fetched_openai_models.clone()));
|
provider_entries.push((name, base_url, api_key, single_provider_models));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider_entries.extend(
|
||||||
|
settings
|
||||||
|
.openai_providers
|
||||||
|
.value()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|provider| {
|
||||||
|
if provider.base_url.trim().is_empty() || provider.models.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
provider.name.clone(),
|
||||||
|
provider.base_url.clone(),
|
||||||
|
provider.api_key.clone(),
|
||||||
|
provider.models.clone(),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if provider_entries.is_empty() {
|
if provider_entries.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -990,20 +1022,21 @@ impl LLMPreferences {
|
|||||||
let mut total_injected = 0;
|
let mut total_injected = 0;
|
||||||
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
||||||
for (provider_name, base_url, api_key, models) in provider_entries {
|
for (provider_name, base_url, api_key, models) in provider_entries {
|
||||||
let client_config = OpenAIClientConfig {
|
|
||||||
base_url: base_url.clone(),
|
|
||||||
api_key: api_key.clone(),
|
|
||||||
model: None, // filled per-request from model_id
|
|
||||||
};
|
|
||||||
|
|
||||||
for model in &models {
|
for model in &models {
|
||||||
if !seen_model_ids.insert(model.model_id.clone()) {
|
if !seen_model_ids.insert(model.model_id.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the routing entry
|
// Register the routing entry
|
||||||
|
let client_config = OpenAIClientConfig {
|
||||||
|
base_url: base_url.clone(),
|
||||||
|
api_key: api_key.clone(),
|
||||||
|
model: None, // filled per-request from model_id
|
||||||
|
max_input_tokens: Some(openai_model_context_size(model)),
|
||||||
|
max_output_tokens: model.max_output_tokens,
|
||||||
|
};
|
||||||
self.openai_provider_routing
|
self.openai_provider_routing
|
||||||
.insert(model.model_id.clone(), client_config.clone());
|
.insert(model.model_id.clone(), client_config);
|
||||||
|
|
||||||
let llm_info = LLMInfo {
|
let llm_info = LLMInfo {
|
||||||
id: LLMId::from(model.model_id.as_str()),
|
id: LLMId::from(model.model_id.as_str()),
|
||||||
@@ -1027,7 +1060,7 @@ impl LLMPreferences {
|
|||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
discount_percentage: None,
|
discount_percentage: None,
|
||||||
context_window: LLMContextWindow::default(),
|
context_window: openai_model_context_window(model),
|
||||||
};
|
};
|
||||||
self.models_by_feature
|
self.models_by_feature
|
||||||
.agent_mode
|
.agent_mode
|
||||||
@@ -1053,10 +1086,7 @@ impl LLMPreferences {
|
|||||||
if feature.choices.is_empty() {
|
if feature.choices.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let default_exists = feature
|
let default_exists = feature.choices.iter().any(|m| m.id == feature.default_id);
|
||||||
.choices
|
|
||||||
.iter()
|
|
||||||
.any(|m| m.id == feature.default_id);
|
|
||||||
if !default_exists {
|
if !default_exists {
|
||||||
let new_default = feature.choices[0].id.clone();
|
let new_default = feature.choices[0].id.clone();
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -1143,17 +1173,36 @@ impl LLMPreferences {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn u32_from_any(value: &serde_json::Value, keys: &[&str]) -> Option<u32> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| value[*key].as_u64())
|
||||||
|
.and_then(|value| u32::try_from(value).ok())
|
||||||
|
}
|
||||||
|
|
||||||
let models: Vec<OpenAIModelConfig> = body["data"]
|
let models: Vec<OpenAIModelConfig> = body["data"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap_or(&vec![])
|
.map(Vec::as_slice)
|
||||||
|
.unwrap_or_default()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|m| {
|
.filter_map(|m| {
|
||||||
let id = m["id"].as_str()?;
|
let id = m["id"].as_str()?;
|
||||||
let context_size = m["max_model_len"]
|
let max_input_tokens = u32_from_any(
|
||||||
.as_u64()
|
m,
|
||||||
.or_else(|| m["context_window"].as_u64())
|
&["max_input_tokens", "input_token_limit", "max_prompt_tokens"],
|
||||||
.or_else(|| m["max_input_tokens"].as_u64())
|
);
|
||||||
.unwrap_or(200_000) as u32;
|
let context_size =
|
||||||
|
u32_from_any(m, &["max_model_len", "context_window", "token_size"])
|
||||||
|
.or(max_input_tokens)
|
||||||
|
.unwrap_or(200_000);
|
||||||
|
let max_output_tokens = u32_from_any(
|
||||||
|
m,
|
||||||
|
&[
|
||||||
|
"max_output_tokens",
|
||||||
|
"output_token_limit",
|
||||||
|
"max_completion_tokens",
|
||||||
|
"max_tokens",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
let display_name = id
|
let display_name = id
|
||||||
.split('/')
|
.split('/')
|
||||||
@@ -1185,8 +1234,13 @@ impl LLMPreferences {
|
|||||||
Some(OpenAIModelConfig {
|
Some(OpenAIModelConfig {
|
||||||
model_id: id.to_string(),
|
model_id: id.to_string(),
|
||||||
display_name,
|
display_name,
|
||||||
vision_supported: m["supports_vision"].as_bool().unwrap_or(false),
|
vision_supported: m["supports_vision"]
|
||||||
|
.as_bool()
|
||||||
|
.or_else(|| m["vision_support"].as_bool())
|
||||||
|
.unwrap_or(false),
|
||||||
context_size,
|
context_size,
|
||||||
|
max_input_tokens,
|
||||||
|
max_output_tokens,
|
||||||
provider,
|
provider,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1200,7 +1254,12 @@ impl LLMPreferences {
|
|||||||
},
|
},
|
||||||
|me, models, ctx| {
|
|me, models, ctx| {
|
||||||
if !models.is_empty() {
|
if !models.is_empty() {
|
||||||
me.fetched_openai_models = models;
|
me.fetched_openai_models = models.clone();
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
if let Err(err) = settings.openai_models.set_value(models, ctx) {
|
||||||
|
report_error!(err.context("Failed to persist fetched OpenAI models"));
|
||||||
|
}
|
||||||
|
});
|
||||||
me.inject_openai_models(ctx);
|
me.inject_openai_models(ctx);
|
||||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||||
}
|
}
|
||||||
@@ -2101,6 +2160,22 @@ fn get_new_agent_mode_choices(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn openai_model_context_size(model: &OpenAIModelConfig) -> u32 {
|
||||||
|
model.max_input_tokens.unwrap_or(model.context_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn openai_model_context_window(model: &OpenAIModelConfig) -> LLMContextWindow {
|
||||||
|
let context_size = openai_model_context_size(model);
|
||||||
|
LLMContextWindow {
|
||||||
|
is_configurable: false,
|
||||||
|
min: context_size,
|
||||||
|
max: context_size,
|
||||||
|
default_max: context_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds synthetic [`LLMInfo`]s from the user's persisted custom endpoints.
|
/// Builds synthetic [`LLMInfo`]s from the user's persisted custom endpoints.
|
||||||
///
|
///
|
||||||
/// One entry per `CustomEndpointModel`. The display label is the **alias** when present,
|
/// One entry per `CustomEndpointModel`. The display label is the **alias** when present,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::network::NetworkStatus;
|
|||||||
use crate::server::cloud_objects::update_manager::UpdateManager;
|
use crate::server::cloud_objects::update_manager::UpdateManager;
|
||||||
use crate::server::server_api::ServerApiProvider;
|
use crate::server::server_api::ServerApiProvider;
|
||||||
use crate::server::sync_queue::SyncQueue;
|
use crate::server::sync_queue::SyncQueue;
|
||||||
|
use crate::settings::{OpenAIModelConfig, OpenAIProviderConfig};
|
||||||
use crate::test_util::settings::initialize_settings_for_tests;
|
use crate::test_util::settings::initialize_settings_for_tests;
|
||||||
use crate::workspaces::team_tester::TeamTesterStatus;
|
use crate::workspaces::team_tester::TeamTesterStatus;
|
||||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||||
@@ -240,6 +241,7 @@ fn custom_endpoint_usage_display_label_resolves_alias_name_and_generic_fallback(
|
|||||||
custom_llms: build_custom_llm_infos(&keys),
|
custom_llms: build_custom_llm_infos(&keys),
|
||||||
custom_model_routers: Vec::new(),
|
custom_model_routers: Vec::new(),
|
||||||
openai_provider_routing: HashMap::new(),
|
openai_provider_routing: HashMap::new(),
|
||||||
|
fetched_openai_models: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -260,6 +262,176 @@ fn custom_endpoint_usage_display_label_resolves_alias_name_and_generic_fallback(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn empty_llm_preferences_for_provider_tests() -> LLMPreferences {
|
||||||
|
LLMPreferences {
|
||||||
|
models_by_feature: ModelsByFeature::default(),
|
||||||
|
last_update: None,
|
||||||
|
base_llm_for_terminal_view: HashMap::new(),
|
||||||
|
custom_llms: Vec::new(),
|
||||||
|
custom_model_routers: Vec::new(),
|
||||||
|
openai_provider_routing: HashMap::new(),
|
||||||
|
fetched_openai_models: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn openai_model(
|
||||||
|
model_id: &str,
|
||||||
|
display_name: &str,
|
||||||
|
context_size: u32,
|
||||||
|
max_input_tokens: Option<u32>,
|
||||||
|
max_output_tokens: Option<u32>,
|
||||||
|
vision_supported: bool,
|
||||||
|
) -> OpenAIModelConfig {
|
||||||
|
OpenAIModelConfig {
|
||||||
|
model_id: model_id.to_string(),
|
||||||
|
display_name: display_name.to_string(),
|
||||||
|
vision_supported,
|
||||||
|
context_size,
|
||||||
|
max_input_tokens,
|
||||||
|
max_output_tokens,
|
||||||
|
provider: Some("openai".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn openai_model_config_accepts_legacy_and_endpoint_field_names() {
|
||||||
|
let model: OpenAIModelConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
model_id = "provider/custom-model"
|
||||||
|
display_name = "Custom Model"
|
||||||
|
vision_support = true
|
||||||
|
token_size = 123456
|
||||||
|
max_input_tokens = 111111
|
||||||
|
max_tokens = 8192
|
||||||
|
provider = "openai"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("model config should parse");
|
||||||
|
|
||||||
|
assert_eq!(model.model_id, "provider/custom-model");
|
||||||
|
assert!(model.vision_supported);
|
||||||
|
assert_eq!(model.context_size, 123_456);
|
||||||
|
assert_eq!(model.max_input_tokens, Some(111_111));
|
||||||
|
assert_eq!(model.max_output_tokens, Some(8_192));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn inject_openai_models_uses_persisted_model_metadata_and_routing() {
|
||||||
|
App::test((), |mut app| async move {
|
||||||
|
initialize_settings_for_tests(&mut app);
|
||||||
|
let mut preferences = empty_llm_preferences_for_provider_tests();
|
||||||
|
|
||||||
|
app.update(|ctx| {
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
settings.openai_enabled.set_value(true, ctx).unwrap();
|
||||||
|
settings
|
||||||
|
.openai_base_url
|
||||||
|
.set_value("https://litellm.example/v1".to_string(), ctx)
|
||||||
|
.unwrap();
|
||||||
|
settings
|
||||||
|
.openai_api_key
|
||||||
|
.set_value("test-key".to_string(), ctx)
|
||||||
|
.unwrap();
|
||||||
|
settings
|
||||||
|
.openai_models
|
||||||
|
.set_value(
|
||||||
|
vec![openai_model(
|
||||||
|
"provider/custom-model",
|
||||||
|
"Custom Model",
|
||||||
|
200_000,
|
||||||
|
Some(128_000),
|
||||||
|
Some(8_192),
|
||||||
|
true,
|
||||||
|
)],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
preferences.inject_openai_models(ctx);
|
||||||
|
|
||||||
|
let model = preferences
|
||||||
|
.models_by_feature
|
||||||
|
.agent_mode
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.find(|model| model.id.as_str() == "provider/custom-model")
|
||||||
|
.expect("configured model should be injected");
|
||||||
|
assert_eq!(model.provider, LLMProvider::LiteLLM);
|
||||||
|
assert_eq!(model.description.as_deref(), Some("LiteLLM"));
|
||||||
|
assert!(model.vision_supported);
|
||||||
|
assert_eq!(model.context_window.default_max, 128_000);
|
||||||
|
assert_eq!(model.context_window.max, 128_000);
|
||||||
|
|
||||||
|
let client_config = preferences
|
||||||
|
.openai_client_config_for_model("provider/custom-model")
|
||||||
|
.expect("configured model should have routing");
|
||||||
|
assert_eq!(client_config.base_url, "https://litellm.example/v1");
|
||||||
|
assert_eq!(client_config.api_key.as_deref(), Some("test-key"));
|
||||||
|
assert_eq!(client_config.max_input_tokens, Some(128_000));
|
||||||
|
assert_eq!(client_config.max_output_tokens, Some(8_192));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn inject_openai_models_uses_multi_provider_models() {
|
||||||
|
App::test((), |mut app| async move {
|
||||||
|
initialize_settings_for_tests(&mut app);
|
||||||
|
let mut preferences = empty_llm_preferences_for_provider_tests();
|
||||||
|
|
||||||
|
app.update(|ctx| {
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
settings.openai_enabled.set_value(true, ctx).unwrap();
|
||||||
|
settings
|
||||||
|
.openai_providers
|
||||||
|
.set_value(
|
||||||
|
vec![OpenAIProviderConfig {
|
||||||
|
name: "Ollama".to_string(),
|
||||||
|
base_url: "http://localhost:11434/v1".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
models: vec![openai_model(
|
||||||
|
"llama3.2",
|
||||||
|
"Llama 3.2",
|
||||||
|
64_000,
|
||||||
|
None,
|
||||||
|
Some(4_096),
|
||||||
|
false,
|
||||||
|
)],
|
||||||
|
}],
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
preferences.inject_openai_models(ctx);
|
||||||
|
|
||||||
|
let model = preferences
|
||||||
|
.models_by_feature
|
||||||
|
.agent_mode
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.find(|model| model.id.as_str() == "llama3.2")
|
||||||
|
.expect("provider model should be injected");
|
||||||
|
assert_eq!(model.description.as_deref(), Some("Ollama"));
|
||||||
|
assert!(!model.vision_supported);
|
||||||
|
assert_eq!(model.context_window.default_max, 64_000);
|
||||||
|
|
||||||
|
let client_config = preferences
|
||||||
|
.openai_client_config_for_model("llama3.2")
|
||||||
|
.expect("provider model should have routing");
|
||||||
|
assert_eq!(client_config.base_url, "http://localhost:11434/v1");
|
||||||
|
assert_eq!(client_config.max_input_tokens, Some(64_000));
|
||||||
|
assert_eq!(client_config.max_output_tokens, Some(4_096));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_llm_infos_skip_endpoints_with_empty_api_key() {
|
fn custom_llm_infos_skip_endpoints_with_empty_api_key() {
|
||||||
let keys = ai::api_keys::ApiKeys {
|
let keys = ai::api_keys::ApiKeys {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ pub struct OpenAIClientConfig {
|
|||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
|
pub max_input_tokens: Option<u32>,
|
||||||
|
pub max_output_tokens: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OpenAIClient {
|
pub struct OpenAIClient {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub fn openai_stream_to_response_events(
|
|||||||
user_query: Option<String>,
|
user_query: Option<String>,
|
||||||
messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
|
||||||
model_id: String,
|
model_id: String,
|
||||||
|
max_context_tokens: Option<u32>,
|
||||||
_tool_result_archive: Vec<ConversationMessage>,
|
_tool_result_archive: Vec<ConversationMessage>,
|
||||||
) -> BoxStream<'static, Event> {
|
) -> BoxStream<'static, Event> {
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@@ -269,7 +270,14 @@ pub fn openai_stream_to_response_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cost = estimate_cost_cents(input_tokens as u32, output_tokens as u32, &model_id);
|
let cost = estimate_cost_cents(input_tokens as u32, output_tokens as u32, &model_id);
|
||||||
let finished_event = build_stream_finished(stop_reason, input_tokens, output_tokens, cost, &model_id);
|
let finished_event = build_stream_finished(
|
||||||
|
stop_reason,
|
||||||
|
input_tokens,
|
||||||
|
output_tokens,
|
||||||
|
cost,
|
||||||
|
&model_id,
|
||||||
|
max_context_tokens,
|
||||||
|
);
|
||||||
yield Ok(finished_event);
|
yield Ok(finished_event);
|
||||||
|
|
||||||
log::info!("[openai] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}");
|
log::info!("[openai] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}");
|
||||||
@@ -409,6 +417,7 @@ fn build_stream_finished(
|
|||||||
output_tokens: i32,
|
output_tokens: i32,
|
||||||
cost_in_cents: f32,
|
cost_in_cents: f32,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
|
max_context_tokens: Option<u32>,
|
||||||
) -> ResponseEvent {
|
) -> ResponseEvent {
|
||||||
let total_tokens = (input_tokens + output_tokens) as u32;
|
let total_tokens = (input_tokens + output_tokens) as u32;
|
||||||
|
|
||||||
@@ -434,12 +443,14 @@ fn build_stream_finished(
|
|||||||
cost_in_cents,
|
cost_in_cents,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let max_context_tokens = context_window_for_model(model_id);
|
let max_context_tokens =
|
||||||
|
max_context_tokens.unwrap_or_else(|| context_window_for_model(model_id));
|
||||||
let context_usage = if max_context_tokens > 0 {
|
let context_usage = if max_context_tokens > 0 {
|
||||||
input_tokens as f32 / max_context_tokens as f32
|
input_tokens as f32 / max_context_tokens as f32
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
}
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use crate::ai::agent::api::ResponseStream;
|
|||||||
use crate::ai::bedrock::request_translator;
|
use crate::ai::bedrock::request_translator;
|
||||||
use crate::ai::provider::types::{ConversationMessage, MessageContent, MessageRole};
|
use crate::ai::provider::types::{ConversationMessage, MessageContent, MessageRole};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 64_000;
|
||||||
|
|
||||||
pub struct TranslatorRequest {
|
pub struct TranslatorRequest {
|
||||||
pub config: OpenAIClientConfig,
|
pub config: OpenAIClientConfig,
|
||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
@@ -98,6 +100,10 @@ pub async fn execute(
|
|||||||
messages.extend(new_input_messages);
|
messages.extend(new_input_messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for message in &mut messages {
|
||||||
|
message.truncate_tool_results_for_provider_request();
|
||||||
|
}
|
||||||
|
|
||||||
sanitize_messages_for_openai(&mut messages);
|
sanitize_messages_for_openai(&mut messages);
|
||||||
|
|
||||||
let system_prompt = request_translator::extract_system_prompt(request);
|
let system_prompt = request_translator::extract_system_prompt(request);
|
||||||
@@ -112,11 +118,17 @@ pub async fn execute(
|
|||||||
|
|
||||||
let user_query_text = request_translator::extract_user_query_text(request);
|
let user_query_text = request_translator::extract_user_query_text(request);
|
||||||
|
|
||||||
|
let max_output_tokens = params
|
||||||
|
.config
|
||||||
|
.max_output_tokens
|
||||||
|
.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS)
|
||||||
|
.min(i32::MAX as u32) as i32;
|
||||||
|
|
||||||
let request_body = build_openai_request(
|
let request_body = build_openai_request(
|
||||||
messages.clone(),
|
messages.clone(),
|
||||||
system_prompt,
|
system_prompt,
|
||||||
tools,
|
tools,
|
||||||
64000,
|
max_output_tokens,
|
||||||
None,
|
None,
|
||||||
&model_id,
|
&model_id,
|
||||||
);
|
);
|
||||||
@@ -140,6 +152,7 @@ pub async fn execute(
|
|||||||
user_query_text,
|
user_query_text,
|
||||||
params.messages_sent.clone(),
|
params.messages_sent.clone(),
|
||||||
model_id,
|
model_id,
|
||||||
|
params.config.max_input_tokens,
|
||||||
params.tool_result_archive,
|
params.tool_result_archive,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
pub const MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST: usize = 64_000;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ConversationMessage {
|
pub struct ConversationMessage {
|
||||||
pub role: MessageRole,
|
pub role: MessageRole,
|
||||||
pub content: MessageContent,
|
pub content: MessageContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConversationMessage {
|
||||||
|
pub fn truncate_tool_results_for_provider_request(&mut self) {
|
||||||
|
truncate_tool_results_in_content(&mut self.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum MessageRole {
|
pub enum MessageRole {
|
||||||
User,
|
User,
|
||||||
@@ -49,3 +57,71 @@ pub struct ToolDefinition {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
pub input_schema: JsonValue,
|
pub input_schema: JsonValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn truncate_tool_results_in_content(content: &mut MessageContent) {
|
||||||
|
match content {
|
||||||
|
MessageContent::Text(_) | MessageContent::ToolUse { .. } => {}
|
||||||
|
MessageContent::ToolResult { content, .. } => truncate_tool_result_text(content),
|
||||||
|
MessageContent::MultiPart(parts) => {
|
||||||
|
for part in parts {
|
||||||
|
if let ContentPart::ToolResult { content, .. } = part {
|
||||||
|
truncate_tool_result_text(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_tool_result_text(content: &mut String) {
|
||||||
|
let char_count = content.chars().count();
|
||||||
|
if char_count <= MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let omitted_chars = char_count.saturating_sub(MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST);
|
||||||
|
let marker = format!("\n... [tool result truncated; omitted {omitted_chars} chars] ...\n");
|
||||||
|
let marker_chars = marker.chars().count();
|
||||||
|
let retained_chars = MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST.saturating_sub(marker_chars);
|
||||||
|
let head_chars = retained_chars / 2;
|
||||||
|
let tail_chars = retained_chars.saturating_sub(head_chars);
|
||||||
|
let head: String = content.chars().take(head_chars).collect();
|
||||||
|
let tail: String = content
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(tail_chars)
|
||||||
|
.collect::<String>()
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.collect();
|
||||||
|
*content = format!("{head}{marker}{tail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncates_large_tool_results_for_provider_request() {
|
||||||
|
let prefix = "start:";
|
||||||
|
let suffix = ":end";
|
||||||
|
let middle = "x".repeat(MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST + 1_000);
|
||||||
|
let mut message = ConversationMessage {
|
||||||
|
role: MessageRole::User,
|
||||||
|
content: MessageContent::ToolResult {
|
||||||
|
tool_use_id: "toolu_1".to_string(),
|
||||||
|
content: format!("{prefix}{middle}{suffix}"),
|
||||||
|
is_error: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
message.truncate_tool_results_for_provider_request();
|
||||||
|
|
||||||
|
let MessageContent::ToolResult { content, .. } = message.content else {
|
||||||
|
panic!("expected tool result");
|
||||||
|
};
|
||||||
|
assert!(content.len() <= MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST + 128);
|
||||||
|
assert!(content.starts_with(prefix));
|
||||||
|
assert!(content.ends_with(suffix));
|
||||||
|
assert!(content.contains("tool result truncated"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-4
@@ -733,8 +733,8 @@ impl schemars::JsonSchema for ToolbarCommandMap {
|
|||||||
std::borrow::Cow::Borrowed("ToolbarCommandMap")
|
std::borrow::Cow::Borrowed("ToolbarCommandMap")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
gen.subschema_for::<HashMap<String, String>>()
|
generator.subschema_for::<HashMap<String, String>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,12 +847,29 @@ pub struct OpenAIModelConfig {
|
|||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
#[schemars(description = "Display name shown in the model picker.")]
|
#[schemars(description = "Display name shown in the model picker.")]
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
#[serde(default)]
|
#[serde(default, alias = "vision_support", alias = "supports_vision")]
|
||||||
#[schemars(description = "Whether the model supports image/vision input.")]
|
#[schemars(description = "Whether the model supports image/vision input.")]
|
||||||
pub vision_supported: bool,
|
pub vision_supported: bool,
|
||||||
#[serde(default = "default_context_size")]
|
#[serde(
|
||||||
|
default = "default_context_size",
|
||||||
|
alias = "token_size",
|
||||||
|
alias = "max_model_len",
|
||||||
|
alias = "context_window"
|
||||||
|
)]
|
||||||
#[schemars(description = "Maximum context window size in tokens.")]
|
#[schemars(description = "Maximum context window size in tokens.")]
|
||||||
pub context_size: u32,
|
pub context_size: u32,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[schemars(description = "Optional maximum input tokens supported by this model.")]
|
||||||
|
pub max_input_tokens: Option<u32>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "output_token_limit",
|
||||||
|
alias = "max_completion_tokens",
|
||||||
|
alias = "max_tokens",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
#[schemars(description = "Optional maximum output tokens to request from this model.")]
|
||||||
|
pub max_output_tokens: Option<u32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schemars(
|
#[schemars(
|
||||||
description = "Optional provider hint (e.g. anthropic, openai, google) for icon display."
|
description = "Optional provider hint (e.g. anthropic, openai, google) for icon display."
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use galaxyui::elements::{
|
|||||||
PositioningAxis, Radius, SavePosition, Stack, Text, XAxisAnchor, YAxisAnchor,
|
PositioningAxis, Radius, SavePosition, Stack, Text, XAxisAnchor, YAxisAnchor,
|
||||||
};
|
};
|
||||||
use galaxyui::presenter::ChildView;
|
use galaxyui::presenter::ChildView;
|
||||||
use galaxyui::{AppContext, SingletonEntity as _};
|
use galaxyui::{AppContext, EntityId, SingletonEntity as _};
|
||||||
use pathfinder_color::ColorU;
|
use pathfinder_color::ColorU;
|
||||||
|
|
||||||
use super::common::{
|
use super::common::{
|
||||||
@@ -22,7 +22,10 @@ use crate::ai::blocklist::agent_view::shortcuts::{
|
|||||||
};
|
};
|
||||||
use crate::ai::blocklist::agent_view::{agent_view_bg_fill, AgentViewState};
|
use crate::ai::blocklist::agent_view::{agent_view_bg_fill, AgentViewState};
|
||||||
use crate::ai::blocklist::InputType;
|
use crate::ai::blocklist::InputType;
|
||||||
|
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
|
||||||
|
use crate::ai::execution_profiles::AIExecutionProfileAppExt;
|
||||||
use crate::ai::harness_availability::HarnessAvailabilityModel;
|
use crate::ai::harness_availability::HarnessAvailabilityModel;
|
||||||
|
use crate::ai::llms::LLMPreferences;
|
||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::context_chips::spacing::{self};
|
use crate::context_chips::spacing::{self};
|
||||||
use crate::editor::position_id_for_cursor;
|
use crate::editor::position_id_for_cursor;
|
||||||
@@ -167,7 +170,9 @@ impl Input {
|
|||||||
.agent_view_state()
|
.agent_view_state()
|
||||||
.active_conversation_id()
|
.active_conversation_id()
|
||||||
{
|
{
|
||||||
if let Some(status_bar) = render_session_status_bar(appearance, app, conv_id) {
|
if let Some(status_bar) =
|
||||||
|
render_session_status_bar(appearance, app, self.terminal_view_id, conv_id)
|
||||||
|
{
|
||||||
column.add_child(status_bar);
|
column.add_child(status_bar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -753,6 +758,7 @@ fn cache_hit_color(pct: f64, theme: &galaxy_core::ui::theme::GalaxyTheme) -> Col
|
|||||||
fn render_session_status_bar(
|
fn render_session_status_bar(
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
app: &AppContext,
|
app: &AppContext,
|
||||||
|
terminal_view_id: EntityId,
|
||||||
conversation_id: crate::ai::agent::conversation::AIConversationId,
|
conversation_id: crate::ai::agent::conversation::AIConversationId,
|
||||||
) -> Option<Box<dyn Element>> {
|
) -> Option<Box<dyn Element>> {
|
||||||
let (cache_read, cache_write, cache_miss, cost_cents, context_usage, current_context) =
|
let (cache_read, cache_write, cache_miss, cost_cents, context_usage, current_context) =
|
||||||
@@ -778,12 +784,20 @@ fn render_session_status_bar(
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_context: u32 = if context_usage > 0.0 {
|
let active_model =
|
||||||
(current_context as f32 / context_usage).round() as u32
|
LLMPreferences::as_ref(app).get_active_base_model(app, Some(terminal_view_id));
|
||||||
} else {
|
let profile_context = AIExecutionProfilesModel::as_ref(app)
|
||||||
200_000
|
.active_profile(Some(terminal_view_id), app)
|
||||||
};
|
.data()
|
||||||
let context_pct = context_usage * 100.0;
|
.context_window_display_value(app);
|
||||||
|
let model_max_context = active_model
|
||||||
|
.context_window
|
||||||
|
.default_max
|
||||||
|
.max(active_model.context_window.max);
|
||||||
|
let max_context = profile_context
|
||||||
|
.or((model_max_context > 0).then_some(model_max_context))
|
||||||
|
.unwrap_or(200_000);
|
||||||
|
let context_pct = context_usage.clamp(0.0, 1.0) * 100.0;
|
||||||
|
|
||||||
let theme = appearance.theme();
|
let theme = appearance.theme();
|
||||||
let font_family = appearance.ui_font_family();
|
let font_family = appearance.ui_font_family();
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ if (!HERMES_PASS) {
|
|||||||
|
|
||||||
const BASE_URL = "https://client.wst.mini-games.tv";
|
const BASE_URL = "https://client.wst.mini-games.tv";
|
||||||
const UPLOAD_KEY = "wst-data/ryan-share/galaxy/Galaxy.zip";
|
const UPLOAD_KEY = "wst-data/ryan-share/galaxy/Galaxy.zip";
|
||||||
|
const INSTALL_SCRIPT_KEY = "wst-data/ryan-share/galaxy/install-galaxy.sh";
|
||||||
const CONTENT_TYPE = "application/zip";
|
const CONTENT_TYPE = "application/zip";
|
||||||
|
const SCRIPT_CONTENT_TYPE = "text/x-shellscript";
|
||||||
|
|
||||||
// Workspace root is three levels up from script/build-and-deploy-hermes/src
|
// Workspace root is three levels up from script/build-and-deploy-hermes/src
|
||||||
const WORKSPACE_ROOT = path.resolve(import.meta.dirname, "../../..");
|
const WORKSPACE_ROOT = path.resolve(import.meta.dirname, "../../..");
|
||||||
@@ -271,6 +273,7 @@ function App() {
|
|||||||
{ label: "Start multipart upload", status: "pending" },
|
{ label: "Start multipart upload", status: "pending" },
|
||||||
{ label: "Upload parts", status: "pending" },
|
{ label: "Upload parts", status: "pending" },
|
||||||
{ label: "Complete upload", status: "pending" },
|
{ label: "Complete upload", status: "pending" },
|
||||||
|
{ label: "Upload install-galaxy.sh", status: "pending" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateStep = useCallback((index: number, update: Partial<Step>) => {
|
const updateStep = useCallback((index: number, update: Partial<Step>) => {
|
||||||
@@ -422,6 +425,76 @@ function App() {
|
|||||||
appendLog(5, `Key: ${UPLOAD_KEY}`);
|
appendLog(5, `Key: ${UPLOAD_KEY}`);
|
||||||
updateStep(5, { status: "done" });
|
updateStep(5, { status: "done" });
|
||||||
|
|
||||||
|
// ─── Step 6: Upload install-galaxy.sh ────────────────────────────
|
||||||
|
updateStep(6, { status: "running" });
|
||||||
|
|
||||||
|
const installScriptPath = path.join(WORKSPACE_ROOT, "script", "install-galaxy.sh");
|
||||||
|
const scriptFileSize = statSync(installScriptPath).size;
|
||||||
|
const scriptFileHash = computeFileHash(installScriptPath);
|
||||||
|
|
||||||
|
const scriptStartRes = await apiPost<StartUploadResponse>(
|
||||||
|
`${BASE_URL}/api/uploads/start`,
|
||||||
|
{
|
||||||
|
key: INSTALL_SCRIPT_KEY,
|
||||||
|
contentType: SCRIPT_CONTENT_TYPE,
|
||||||
|
fileSize: scriptFileSize,
|
||||||
|
fileHash: scriptFileHash,
|
||||||
|
},
|
||||||
|
authHeaders(token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const scriptCompletedParts: { partNumber: number; etag: string }[] = [];
|
||||||
|
let scriptBytesUploaded = 0;
|
||||||
|
|
||||||
|
updateStep(6, { status: "running", progress: { bytes: 0, totalBytes: scriptFileSize } });
|
||||||
|
|
||||||
|
for (const part of scriptStartRes.urls) {
|
||||||
|
const prevBytes = scriptBytesUploaded;
|
||||||
|
const etag = await uploadPart(
|
||||||
|
part.url,
|
||||||
|
installScriptPath,
|
||||||
|
part.partNumber,
|
||||||
|
scriptStartRes.partSize,
|
||||||
|
scriptStartRes.totalParts,
|
||||||
|
(partBytes) => {
|
||||||
|
updateStep(6, {
|
||||||
|
status: "running",
|
||||||
|
progress: { bytes: prevBytes + partBytes, totalBytes: scriptFileSize },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const thisPartSize = part.partNumber === scriptStartRes.totalParts
|
||||||
|
? scriptFileSize - (scriptStartRes.partSize * (scriptStartRes.totalParts - 1))
|
||||||
|
: scriptStartRes.partSize;
|
||||||
|
scriptBytesUploaded += thisPartSize;
|
||||||
|
|
||||||
|
scriptCompletedParts.push({ partNumber: part.partNumber, etag });
|
||||||
|
|
||||||
|
await apiPost(
|
||||||
|
`${BASE_URL}/api/uploads/part-complete`,
|
||||||
|
{
|
||||||
|
uploadId: scriptStartRes.uploadId,
|
||||||
|
partNumber: part.partNumber,
|
||||||
|
etag,
|
||||||
|
},
|
||||||
|
authHeaders(token)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiPost(
|
||||||
|
`${BASE_URL}/api/uploads/complete`,
|
||||||
|
{
|
||||||
|
key: INSTALL_SCRIPT_KEY,
|
||||||
|
uploadId: scriptStartRes.uploadId,
|
||||||
|
parts: scriptCompletedParts.sort((a, b) => a.partNumber - b.partNumber),
|
||||||
|
},
|
||||||
|
authHeaders(token)
|
||||||
|
);
|
||||||
|
|
||||||
|
appendLog(6, `Key: ${INSTALL_SCRIPT_KEY}`);
|
||||||
|
updateStep(6, { status: "done", detail: `${(scriptFileSize / 1024).toFixed(1)} KB`, progress: undefined });
|
||||||
|
|
||||||
// Cleanup zip
|
// Cleanup zip
|
||||||
execSync(`rm -f "${zipPath}"`, { stdio: "pipe" });
|
execSync(`rm -f "${zipPath}"`, { stdio: "pipe" });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ unzip -q "$TMP_DIR/Galaxy.zip" -d "$TMP_DIR" || fail "Failed to extract Galaxy.z
|
|||||||
rm -rf "$TMP_DIR/__MACOSX"
|
rm -rf "$TMP_DIR/__MACOSX"
|
||||||
find "$TMP_DIR/$APP_NAME" -name '.DS_Store' -delete 2>/dev/null || true
|
find "$TMP_DIR/$APP_NAME" -name '.DS_Store' -delete 2>/dev/null || true
|
||||||
find "$TMP_DIR/$APP_NAME" -name '._*' -delete 2>/dev/null || true
|
find "$TMP_DIR/$APP_NAME" -name '._*' -delete 2>/dev/null || true
|
||||||
# Remove custom Icon file from bundle root (causes "unsealed contents" codesign error)
|
|
||||||
rm -f "$TMP_DIR/$APP_NAME/Icon"$'\r' "$TMP_DIR/$APP_NAME/Icon" 2>/dev/null || true
|
|
||||||
|
|
||||||
if [[ ! -d "$TMP_DIR/$APP_NAME" ]]; then
|
if [[ ! -d "$TMP_DIR/$APP_NAME" ]]; then
|
||||||
fail "$APP_NAME not found after extraction."
|
fail "$APP_NAME not found after extraction."
|
||||||
|
|||||||
Reference in New Issue
Block a user