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:
Ryan Ward
2026-07-15 16:17:13 -05:00
parent af5315313d
commit e9a9a4c30f
19 changed files with 523 additions and 61 deletions
+1 -1
View File
@@ -2250,7 +2250,7 @@ impl AIConversation {
if let Some(usage_metadata) = usage_metadata {
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.platform_credits_spent =
usage_metadata.platform_credits_spent;
+2 -1
View File
@@ -678,7 +678,8 @@ pub fn build_stream_finished(
/ max_context_tokens as f32
} else {
0.0
};
}
.clamp(0.0, 1.0);
#[allow(deprecated)]
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
+4
View File
@@ -98,6 +98,10 @@ pub async fn execute(
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);
let system_prompt = request_translator::extract_system_prompt(request);
@@ -1942,7 +1942,7 @@ impl AgentInputFooter {
if let Some(conversation) =
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 remaining_pct = ((1.0 - usage) * 100.0).round() as i32;
+14 -11
View File
@@ -3139,17 +3139,20 @@ impl BlocklistAIController {
// Check if this error is eligible for corrective retry.
// Similar to loop detection, inject a message telling the LLM
// 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 is_corrective_retry_candidate = !matches!(
e.as_ref(),
AIApiError::QuotaLimit { .. }
) && (error_str.contains("ValidationException")
|| error_str.contains("validation")
|| error_str.contains("context window")
|| error_str.contains("too many tokens")
|| error_str.contains("input is too long")
|| error_str.contains("throttl")
|| error_str.contains("ThrottlingException"));
let is_proxy_config_error = error_str.contains("cache_control")
|| error_str.contains("tool_use` ids were found without")
|| error_str.contains("BadRequestError");
let is_corrective_retry_candidate = !is_proxy_config_error
&& !matches!(e.as_ref(), AIApiError::QuotaLimit { .. })
&& (error_str.contains("ValidationException")
|| error_str.contains("context window")
|| error_str.contains("too many tokens")
|| error_str.contains("input is too long")
|| error_str.contains("throttl")
|| error_str.contains("ThrottlingException"));
const MAX_ERROR_RETRIES: usize = 2;
let retry_count = self
@@ -4026,7 +4029,7 @@ impl BlocklistAIController {
let max_ctx = context_window_for_model(&active_model_id);
let new_usage =
(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
.set_current_context_tokens(summary_tokens + remaining_msgs_tokens);
@@ -185,6 +185,8 @@ impl ResponseStream {
base_url: client_config.base_url.clone(),
api_key: client_config.api_key.clone(),
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));
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
{
format!("{context_usage_pct:.2}%")
@@ -540,7 +541,7 @@ impl ConversationUsageView {
)
.with_child(
ConstrainedBox::new(render_context_window_usage_icon(
self.usage_info.context_window_usage,
context_window_usage,
theme,
None,
))
+100 -25
View File
@@ -602,7 +602,7 @@ pub struct LLMPreferences {
#[cfg(not(target_family = "wasm"))]
openai_provider_routing: HashMap<String, super::openai::client::OpenAIClientConfig>,
/// 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"))]
fetched_openai_models: Vec<OpenAIModelConfig>,
}
@@ -670,10 +670,18 @@ impl LLMPreferences {
AISettingsChangedEvent::BedrockEnabled { .. }
| AISettingsChangedEvent::OpenAIEnabled { .. }
| AISettingsChangedEvent::OpenAIBaseUrl { .. }
| AISettingsChangedEvent::OpenAIApiKey { .. }
| AISettingsChangedEvent::OpenAIModels { .. }
| AISettingsChangedEvent::OpenAIProviders { .. }
) {
me.inject_bedrock_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);
}
// Safety: ensure the default model is still present in choices.
@@ -961,11 +969,17 @@ impl LLMPreferences {
return;
}
// Models come exclusively from the in-memory /models endpoint fetch.
let mut provider_entries: Vec<(String, String, Option<String>, Vec<OpenAIModelConfig>)> =
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 api_key = {
let key = settings.openai_api_key.value().clone();
@@ -980,9 +994,27 @@ impl LLMPreferences {
} else {
"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() {
return;
}
@@ -990,20 +1022,21 @@ impl LLMPreferences {
let mut total_injected = 0;
let mut seen_model_ids: HashSet<String> = HashSet::new();
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 {
if !seen_model_ids.insert(model.model_id.clone()) {
continue;
}
// 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
.insert(model.model_id.clone(), client_config.clone());
.insert(model.model_id.clone(), client_config);
let llm_info = LLMInfo {
id: LLMId::from(model.model_id.as_str()),
@@ -1027,7 +1060,7 @@ impl LLMPreferences {
},
)]),
discount_percentage: None,
context_window: LLMContextWindow::default(),
context_window: openai_model_context_window(model),
};
self.models_by_feature
.agent_mode
@@ -1053,10 +1086,7 @@ impl LLMPreferences {
if feature.choices.is_empty() {
return;
}
let default_exists = feature
.choices
.iter()
.any(|m| m.id == feature.default_id);
let default_exists = feature.choices.iter().any(|m| m.id == feature.default_id);
if !default_exists {
let new_default = feature.choices[0].id.clone();
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"]
.as_array()
.unwrap_or(&vec![])
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
.filter_map(|m| {
let id = m["id"].as_str()?;
let context_size = m["max_model_len"]
.as_u64()
.or_else(|| m["context_window"].as_u64())
.or_else(|| m["max_input_tokens"].as_u64())
.unwrap_or(200_000) as u32;
let max_input_tokens = u32_from_any(
m,
&["max_input_tokens", "input_token_limit", "max_prompt_tokens"],
);
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
.split('/')
@@ -1185,8 +1234,13 @@ impl LLMPreferences {
Some(OpenAIModelConfig {
model_id: id.to_string(),
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,
max_input_tokens,
max_output_tokens,
provider,
})
})
@@ -1200,7 +1254,12 @@ impl LLMPreferences {
},
|me, models, ctx| {
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);
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
}
@@ -2101,6 +2160,22 @@ fn get_new_agent_mode_choices(
.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.
///
/// One entry per `CustomEndpointModel`. The display label is the **alias** when present,
+172
View File
@@ -10,6 +10,7 @@ use crate::network::NetworkStatus;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::settings::{OpenAIModelConfig, OpenAIProviderConfig};
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team_tester::TeamTesterStatus;
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_model_routers: Vec::new(),
openai_provider_routing: HashMap::new(),
fetched_openai_models: Vec::new(),
};
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]
fn custom_llm_infos_skip_endpoints_with_empty_api_key() {
let keys = ai::api_keys::ApiKeys {
+2
View File
@@ -9,6 +9,8 @@ pub struct OpenAIClientConfig {
pub base_url: String,
pub api_key: Option<String>,
pub model: Option<String>,
pub max_input_tokens: Option<u32>,
pub max_output_tokens: Option<u32>,
}
pub struct OpenAIClient {
+14 -3
View File
@@ -30,6 +30,7 @@ pub fn openai_stream_to_response_events(
user_query: Option<String>,
messages_sent: Arc<Mutex<Vec<ConversationMessage>>>,
model_id: String,
max_context_tokens: Option<u32>,
_tool_result_archive: Vec<ConversationMessage>,
) -> BoxStream<'static, Event> {
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 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);
log::info!("[openai] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}");
@@ -409,6 +417,7 @@ fn build_stream_finished(
output_tokens: i32,
cost_in_cents: f32,
model_id: &str,
max_context_tokens: Option<u32>,
) -> ResponseEvent {
let total_tokens = (input_tokens + output_tokens) as u32;
@@ -434,12 +443,14 @@ fn build_stream_finished(
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 {
input_tokens as f32 / max_context_tokens as f32
} else {
0.0
};
}
.clamp(0.0, 1.0);
#[allow(deprecated)]
let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata {
+14 -1
View File
@@ -10,6 +10,8 @@ use crate::ai::agent::api::ResponseStream;
use crate::ai::bedrock::request_translator;
use crate::ai::provider::types::{ConversationMessage, MessageContent, MessageRole};
const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 64_000;
pub struct TranslatorRequest {
pub config: OpenAIClientConfig,
pub model_id: String,
@@ -98,6 +100,10 @@ pub async fn execute(
messages.extend(new_input_messages);
}
for message in &mut messages {
message.truncate_tool_results_for_provider_request();
}
sanitize_messages_for_openai(&mut messages);
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 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(
messages.clone(),
system_prompt,
tools,
64000,
max_output_tokens,
None,
&model_id,
);
@@ -140,6 +152,7 @@ pub async fn execute(
user_query_text,
params.messages_sent.clone(),
model_id,
params.config.max_input_tokens,
params.tool_result_archive,
);
+76
View File
@@ -1,11 +1,19 @@
use serde_json::Value as JsonValue;
pub const MAX_TOOL_RESULT_CHARS_FOR_PROVIDER_REQUEST: usize = 64_000;
#[derive(Clone, Debug)]
pub struct ConversationMessage {
pub role: MessageRole,
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)]
pub enum MessageRole {
User,
@@ -49,3 +57,71 @@ pub struct ToolDefinition {
pub description: String,
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
View File
@@ -733,8 +733,8 @@ impl schemars::JsonSchema for ToolbarCommandMap {
std::borrow::Cow::Borrowed("ToolbarCommandMap")
}
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
gen.subschema_for::<HashMap<String, String>>()
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
generator.subschema_for::<HashMap<String, String>>()
}
}
@@ -847,12 +847,29 @@ pub struct OpenAIModelConfig {
pub model_id: String,
#[schemars(description = "Display name shown in the model picker.")]
pub display_name: String,
#[serde(default)]
#[serde(default, alias = "vision_support", alias = "supports_vision")]
#[schemars(description = "Whether the model supports image/vision input.")]
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.")]
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)]
#[schemars(
description = "Optional provider hint (e.g. anthropic, openai, google) for icon display."
+22 -8
View File
@@ -8,7 +8,7 @@ use galaxyui::elements::{
PositioningAxis, Radius, SavePosition, Stack, Text, XAxisAnchor, YAxisAnchor,
};
use galaxyui::presenter::ChildView;
use galaxyui::{AppContext, SingletonEntity as _};
use galaxyui::{AppContext, EntityId, SingletonEntity as _};
use pathfinder_color::ColorU;
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::InputType;
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::AIExecutionProfileAppExt;
use crate::ai::harness_availability::HarnessAvailabilityModel;
use crate::ai::llms::LLMPreferences;
use crate::appearance::Appearance;
use crate::context_chips::spacing::{self};
use crate::editor::position_id_for_cursor;
@@ -167,7 +170,9 @@ impl Input {
.agent_view_state()
.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);
}
}
@@ -753,6 +758,7 @@ fn cache_hit_color(pct: f64, theme: &galaxy_core::ui::theme::GalaxyTheme) -> Col
fn render_session_status_bar(
appearance: &Appearance,
app: &AppContext,
terminal_view_id: EntityId,
conversation_id: crate::ai::agent::conversation::AIConversationId,
) -> Option<Box<dyn Element>> {
let (cache_read, cache_write, cache_miss, cost_cents, context_usage, current_context) =
@@ -778,12 +784,20 @@ fn render_session_status_bar(
0.0
};
let max_context: u32 = if context_usage > 0.0 {
(current_context as f32 / context_usage).round() as u32
} else {
200_000
};
let context_pct = context_usage * 100.0;
let active_model =
LLMPreferences::as_ref(app).get_active_base_model(app, Some(terminal_view_id));
let profile_context = AIExecutionProfilesModel::as_ref(app)
.active_profile(Some(terminal_view_id), app)
.data()
.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 font_family = appearance.ui_font_family();