v1.3.0: Bedrock translator refactor, usage metrics, session restore fixes, and predefined rules
Major changes: - **Bedrock translator architecture**: Extract orchestration logic from `impl.rs` into a dedicated `translator.rs` module. Rename `convert_request.rs` → `request_translator.rs` and `stream.rs` → `response_translator.rs` for clarity. Remove `tool_docs.rs` (inlined). Remove `fallback_to_warp` setting and server fallback path — Bedrock is now the sole backend. - **Unknown tool handling**: The response translator now detects hallucinated/unknown tool calls from the model and synthesizes error tool_results so the conversation doesn't deadlock waiting for a result that will never come. - **Usage display overhaul**: Replace credit-based usage display with detailed token metrics showing context window %, cache hit rate (read/write/miss), and estimated cost in dollars. Add `total_input_tokens`, `total_cache_read_tokens`, `total_cache_write_tokens`, and `cache_miss_tokens` accessors to `AIConversation`. - **Predefined rules system**: Add `predefined_rules.rs` with 11 system-defined behavioral rules that are auto-seeded on first launch. Add "Add Predefined Rules" button to the Rules UI for re-adding them later. Track seeding state via `has_seeded_predefined_rules` setting. - **Session restore improvements**: Rename database file from `warp.sqlite` to `galaxy.sqlite` with automatic migration from both same-directory and state_dir legacy paths. Improve CWD persistence by falling back to `session_startup_path` for agent-mode and fresh tabs. Add extensive session-save/restore logging. - **Shell bootstrap rebrand**: Rename `WARP_INITIAL_WORKING_DIR` environment variable to `GALAXY_INITIAL_WORKING_DIR` across bash, zsh, and fish bootstrap scripts. - **Model defaults**: Change default Bedrock model from Opus 4.7 to Opus 4.6. Add `context_window_for_model()` helper with model-aware context sizes. Remove `is_bedrock_model()` (no longer needed without server fallback). - **User query persistence**: The response translator now emits a `UserQuery` proto message at stream start so the user's prompt persists across sessions for conversation titles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ec99146ccc
commit
eaa2ddc75e
@@ -18,7 +18,6 @@ use crate::ai::blocklist::block::view_impl::common::{
|
||||
use crate::ai::blocklist::inline_action::aws_bedrock_credentials_error::AwsBedrockCredentialsErrorView;
|
||||
use crate::ai::blocklist::inline_action::create_or_edit_document::CreateOrEditDocumentAction;
|
||||
use crate::ai::blocklist::secret_redaction::SecretRedactionState;
|
||||
use crate::ai::blocklist::view_util::format_credits;
|
||||
use crate::ai::skills::SkillOpenOrigin;
|
||||
use crate::ai::skills::{
|
||||
icon_override_for_skill_name, render_skill_button, skill_path_from_file_path,
|
||||
@@ -3195,13 +3194,8 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
return Empty::new().finish();
|
||||
};
|
||||
|
||||
// If this conversation has no usage metadata (e.g. a forked conversation from
|
||||
// mid-way through a prior conversation where the server did not send
|
||||
// ConversationUsageMetadata), avoid rendering the usage button entirely.
|
||||
let has_any_usage = conversation.credits_spent() > 0.0
|
||||
|| conversation.credits_spent_for_last_block().is_some()
|
||||
|| !conversation.token_usage().is_empty()
|
||||
|| conversation.tool_usage_metadata().total_tool_calls() > 0;
|
||||
let has_any_usage = conversation.total_tokens() > 0
|
||||
|| conversation.total_cost_cents() > 0.0;
|
||||
if !has_any_usage {
|
||||
return Empty::new().finish();
|
||||
}
|
||||
@@ -3215,29 +3209,37 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
Icon::ChevronRight
|
||||
};
|
||||
|
||||
let total_credits_spent = conversation.credits_spent();
|
||||
let mut credit_usage_text = format_credits(total_credits_spent);
|
||||
if let Some(credits_spent_for_last_block) = conversation.credits_spent_for_last_block() {
|
||||
// Only show the credits spent for the last block if it is different from the total credits spent
|
||||
// and we spent a non-zero amount of credits for the last block.
|
||||
// Avoid showing the credits spent for the last block if the request failed, as we refund user
|
||||
// credits in that case (so no credits were in fact spent).
|
||||
if credits_spent_for_last_block > 0.0
|
||||
&& total_credits_spent != credits_spent_for_last_block
|
||||
&& props.model.status(app).error().is_none()
|
||||
{
|
||||
// If the first part of the decimal is 0, we just display the whole number.
|
||||
if credits_spent_for_last_block.fract() < 0.1 {
|
||||
credit_usage_text = format!(
|
||||
"{credit_usage_text} (+{})",
|
||||
credits_spent_for_last_block.trunc() as i32
|
||||
);
|
||||
} else {
|
||||
credit_usage_text =
|
||||
format!("{credit_usage_text} (+{credits_spent_for_last_block:.1})");
|
||||
}
|
||||
}
|
||||
}
|
||||
let context_usage = conversation.context_window_usage();
|
||||
let total_input = conversation.total_input_tokens();
|
||||
let cache_read = conversation.total_cache_read_tokens();
|
||||
let cache_write = conversation.total_cache_write_tokens();
|
||||
let cache_miss = conversation.cache_miss_tokens();
|
||||
let cost_cents = conversation.total_cost_cents();
|
||||
|
||||
let max_context: u32 = if context_usage > 0.0 {
|
||||
(total_input as f32 / context_usage).round() as u32
|
||||
} else {
|
||||
200_000
|
||||
};
|
||||
let context_pct = context_usage * 100.0;
|
||||
let cache_total = cache_read + cache_write + cache_miss;
|
||||
let cache_hit_pct = if cache_total > 0 {
|
||||
(cache_read as f64 / cache_total as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let usage_text = format!(
|
||||
"Context: {:.1}% ({} / {}) | Cache: {:.1}% (R: {}, W: {}, M: {}) | Cost: ${:.2}",
|
||||
context_pct,
|
||||
format_token_count(total_input),
|
||||
format_token_count(max_context),
|
||||
cache_hit_pct,
|
||||
format_token_count(cache_read),
|
||||
format_token_count(cache_write),
|
||||
format_token_count(cache_miss),
|
||||
cost_cents / 100.0,
|
||||
);
|
||||
|
||||
let icon_size = icon_size(app);
|
||||
let button_row = Flex::row()
|
||||
@@ -3246,7 +3248,7 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
credit_usage_text,
|
||||
usage_text,
|
||||
appearance.ui_font_family(),
|
||||
appearance.monospace_font_size(),
|
||||
)
|
||||
@@ -3265,7 +3267,6 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
// Expansion icon
|
||||
ConstrainedBox::new(
|
||||
expansion_icon
|
||||
.to_galaxyui_icon(
|
||||
@@ -3299,10 +3300,9 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
.with_background(background)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)));
|
||||
|
||||
// Show tooltip on hover or while clicked
|
||||
let mut stack = Stack::new().with_child(content.finish());
|
||||
let tooltip = ui_builder
|
||||
.tool_tip("Show credit usage details".to_string())
|
||||
.tool_tip("Show usage details".to_string())
|
||||
.build()
|
||||
.finish();
|
||||
stack.add_positioned_overlay_child(
|
||||
@@ -3328,6 +3328,16 @@ fn render_usage_button(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn format_token_count(tokens: u32) -> String {
|
||||
if tokens >= 1_000_000 {
|
||||
format!("{:.1}M", tokens as f64 / 1_000_000.0)
|
||||
} else if tokens >= 1_000 {
|
||||
format!("{:.1}k", tokens as f64 / 1_000.0)
|
||||
} else {
|
||||
format!("{tokens}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_icon<V: View>(
|
||||
action_id: &AIAgentActionId,
|
||||
action_model: &ModelHandle<BlocklistAIActionModel>,
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::{
|
||||
},
|
||||
network::NetworkStatus,
|
||||
report_error, send_telemetry_from_ctx,
|
||||
server::server_api::ServerApiProvider,
|
||||
settings::ai::AISettings,
|
||||
};
|
||||
use settings::Setting;
|
||||
@@ -100,7 +99,6 @@ impl ResponseStream {
|
||||
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||
fallback_to_warp: *settings.bedrock_fallback_to_warp.value(),
|
||||
}.with_external_fallbacks())
|
||||
}
|
||||
|
||||
@@ -110,7 +108,6 @@ impl ResponseStream {
|
||||
can_attempt_resume_on_error: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||
let (cancellation_tx, cancellation_rx) = oneshot::channel();
|
||||
let start_time = Local::now();
|
||||
|
||||
@@ -119,13 +116,7 @@ impl ResponseStream {
|
||||
let params_clone = params.clone();
|
||||
let _ = ctx.spawn(
|
||||
async move {
|
||||
generate_multi_agent_output(
|
||||
server_api,
|
||||
bedrock_config,
|
||||
params_clone,
|
||||
cancellation_rx,
|
||||
)
|
||||
.await
|
||||
generate_multi_agent_output(bedrock_config, params_clone, cancellation_rx).await
|
||||
},
|
||||
move |me, stream, ctx| {
|
||||
me.handle_response_stream_result(request_id, stream, ctx);
|
||||
@@ -194,11 +185,9 @@ impl ResponseStream {
|
||||
self.current_request_id = Some(request_id);
|
||||
let params = self.params.clone();
|
||||
let bedrock_config = Self::bedrock_config_if_applicable(params.model.as_str(), ctx);
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||
let _ = ctx.spawn(
|
||||
async move {
|
||||
generate_multi_agent_output(server_api, bedrock_config, params, cancellation_rx)
|
||||
.await
|
||||
generate_multi_agent_output(bedrock_config, params, cancellation_rx).await
|
||||
},
|
||||
move |me, stream, ctx| {
|
||||
me.handle_response_stream_result(request_id, stream, ctx);
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(crate) use view_util::{
|
||||
NEW_AGENT_PANE_LABEL,
|
||||
};
|
||||
|
||||
pub(crate) use view_util::{format_credits, format_token_count};
|
||||
pub(crate) use view_util::{format_cost_cents, format_credits, format_token_count};
|
||||
|
||||
pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation};
|
||||
pub use block::keyboard_navigable_buttons;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ai::blocklist::usage::render_context_window_usage_icon;
|
||||
use crate::ai::blocklist::view_util::{format_cost_cents, format_credits, format_token_count};
|
||||
use crate::ai::blocklist::view_util::{format_cost_cents, format_token_count};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::persistence::model::{
|
||||
token_usage_category_display_name, ModelTokenUsage, FULL_TERMINAL_USE_CATEGORY,
|
||||
@@ -26,10 +26,6 @@ pub enum DisplayMode {
|
||||
}
|
||||
|
||||
pub struct ConversationUsageInfo {
|
||||
pub credits_spent: f32,
|
||||
// Credits spent over the last block, where the block comprises
|
||||
// all agent outputs since the most recent user input.
|
||||
pub credits_spent_for_last_block: Option<f32>,
|
||||
pub tool_calls: i32,
|
||||
pub models: Vec<ModelTokenUsage>,
|
||||
pub context_window_usage: f32,
|
||||
@@ -144,28 +140,10 @@ impl ConversationUsageView {
|
||||
));
|
||||
values.push(render_section_header("".to_string(), appearance));
|
||||
|
||||
if self.display_mode == DisplayMode::Footer
|
||||
&& self.usage_info.credits_spent_for_last_block.is_some()
|
||||
{
|
||||
let last_block_credits = self.usage_info.credits_spent_for_last_block.unwrap();
|
||||
labels.push(render_label_text(
|
||||
"Credits spent (last response)",
|
||||
appearance,
|
||||
));
|
||||
if self.usage_info.estimated_cost_cents > 0.0 {
|
||||
labels.push(render_label_text("Estimated cost", appearance));
|
||||
values.push(render_value_text(
|
||||
format_credits(last_block_credits),
|
||||
appearance,
|
||||
));
|
||||
|
||||
labels.push(render_label_text("Credits spent (total)", appearance));
|
||||
values.push(render_value_text(
|
||||
format_credits(self.usage_info.credits_spent),
|
||||
appearance,
|
||||
));
|
||||
} else {
|
||||
labels.push(render_label_text("Credits spent", appearance));
|
||||
values.push(render_value_text(
|
||||
format_credits(self.usage_info.credits_spent),
|
||||
format_cost_cents(self.usage_info.estimated_cost_cents),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
@@ -307,14 +285,6 @@ impl ConversationUsageView {
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
|
||||
if self.usage_info.estimated_cost_cents > 0.0 {
|
||||
labels.push(render_label_text("Estimated cost", appearance));
|
||||
values.push(render_value_text(
|
||||
format_cost_cents(self.usage_info.estimated_cost_cents),
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
labels.push(render_label_text("Context window used", appearance));
|
||||
|
||||
Reference in New Issue
Block a user