Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 75 additions & 6 deletions codex-rs/state/src/runtime/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ INSERT INTO usage_sample_contributors (
.bind(until)
.fetch_one(self.pool.as_ref())
.await?;
let apps = self
.read_usage_contributors(since, until, UsageContributorKind::App, total_tokens)
.await?;
let plugins = suppress_app_mirror_plugins(
&apps,
self.read_usage_contributors(since, until, UsageContributorKind::Plugin, total_tokens)
.await?,
);
let mut report = UsageReport {
range,
generated_at: now,
Expand All @@ -146,9 +154,7 @@ INSERT INTO usage_sample_contributors (
agent_tasks: self
.read_agent_task_usage(since, until, total_tokens)
.await?,
apps: self
.read_usage_contributors(since, until, UsageContributorKind::App, total_tokens)
.await?,
apps,
mcp_servers: self
.read_usage_contributors(
since,
Expand All @@ -157,9 +163,7 @@ INSERT INTO usage_sample_contributors (
total_tokens,
)
.await?,
plugins: self
.read_usage_contributors(since, until, UsageContributorKind::Plugin, total_tokens)
.await?,
plugins,
};
report.headline = usage_headline(&report);
Ok(report)
Expand Down Expand Up @@ -423,6 +427,17 @@ fn usage_headline(report: &UsageReport) -> Option<UsageHeadline> {
Some(UsageHeadline { entry, note })
}

fn suppress_app_mirror_plugins(apps: &[UsageEntry], plugins: Vec<UsageEntry>) -> Vec<UsageEntry> {
plugins
.into_iter()
.filter(|plugin| {
!apps.iter().any(|app| {
app.label == plugin.label && app.attributed_tokens == plugin.attributed_tokens
})
})
.collect()
}

fn agent_task_label(source: &str) -> String {
let parsed_source = serde_json::from_str(source)
.or_else(|_| serde_json::from_value::<SessionSource>(Value::String(source.to_string())));
Expand Down Expand Up @@ -714,6 +729,44 @@ mod tests {
);
}

#[test]
fn suppress_app_mirror_plugins_keeps_non_mirror_plugins() {
let apps = vec![usage_entry(
UsageContributorKind::App,
"connector_gmail",
"Gmail",
/*attributed_tokens*/ 340,
/*percent_of_usage*/ 10,
)];
let plugins = vec![
usage_entry(
UsageContributorKind::Plugin,
"Gmail",
"Gmail",
/*attributed_tokens*/ 340,
/*percent_of_usage*/ 10,
),
usage_entry(
UsageContributorKind::Plugin,
"Workspace",
"Workspace",
/*attributed_tokens*/ 500,
/*percent_of_usage*/ 15,
),
];

assert_eq!(
suppress_app_mirror_plugins(&apps, plugins),
vec![usage_entry(
UsageContributorKind::Plugin,
"Workspace",
"Workspace",
/*attributed_tokens*/ 500,
/*percent_of_usage*/ 15,
)]
);
}

#[tokio::test]
async fn usage_report_groups_agent_tasks_by_subagent_source() {
let (codex_home, runtime) = usage_runtime().await;
Expand Down Expand Up @@ -1127,6 +1180,22 @@ mod tests {
}
}

fn usage_entry(
kind: UsageContributorKind,
id: &str,
label: &str,
attributed_tokens: i64,
percent_of_usage: u8,
) -> UsageEntry {
UsageEntry {
kind,
id: id.to_string(),
label: label.to_string(),
attributed_tokens,
percent_of_usage,
}
}

async fn usage_sample_count(runtime: &StateRuntime) -> i64 {
sqlx::query_scalar("SELECT COUNT(*) FROM usage_samples")
.fetch_one(runtime.pool.as_ref())
Expand Down
33 changes: 33 additions & 0 deletions codex-rs/tui/src/app/background_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use codex_app_server_protocol::MarketplaceRemoveParams;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::MarketplaceUpgradeParams;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::UsageRange;
use codex_app_server_protocol::UsageReadParams;
use codex_app_server_protocol::UsageReadResponse;

use codex_app_server_protocol::RequestId;

Expand Down Expand Up @@ -143,6 +146,22 @@ impl App {
});
}

pub(super) fn fetch_usage(
&mut self,
app_server: &AppServerSession,
request_id: u64,
range: UsageRange,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = fetch_usage(request_handle, range)
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::UsageLoaded { request_id, result });
});
}

pub(super) fn fetch_hooks_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
Expand Down Expand Up @@ -597,6 +616,20 @@ impl App {
}
}

async fn fetch_usage(
request_handle: AppServerRequestHandle,
range: UsageRange,
) -> Result<UsageReadResponse> {
let request_id = RequestId::String(format!("usage-read-{}", uuid::Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::UsageRead {
request_id,
params: UsageReadParams { range },
})
.await
.map_err(Into::into)
}

pub(super) async fn fetch_all_mcp_server_statuses(
request_handle: AppServerRequestHandle,
detail: McpServerStatusDetail,
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ impl App {
AppEvent::FetchPluginsList { cwd } => {
self.fetch_plugins_list(app_server, cwd);
}
AppEvent::FetchUsage { request_id, range } => {
self.fetch_usage(app_server, request_id, range);
}
AppEvent::UsageLoaded { request_id, result } => {
self.chat_widget.on_usage_loaded(request_id, result);
}
AppEvent::FetchHooksList { cwd } => {
self.fetch_hooks_list(app_server, cwd);
}
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,16 @@ pub(crate) enum AppEvent {
cwd: PathBuf,
},

FetchUsage {
request_id: u64,
range: codex_app_server_protocol::UsageRange,
},

UsageLoaded {
request_id: u64,
result: Result<codex_app_server_protocol::UsageReadResponse, String>,
},

/// Fetch lifecycle hook inventory for the provided working directory.
FetchHooksList {
cwd: PathBuf,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/bottom_pane/slash_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ mod tests {
SlashCommand::Diff,
SlashCommand::Mention,
SlashCommand::Status,
SlashCommand::Usage,
]
);
}
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ use self::skills::find_app_mentions;
use self::skills::find_skill_mentions_with_tool_mentions;
use self::skills::is_app_mentionable;
mod plugins;
mod usage;
use self::plugins::PluginInstallAuthFlowState;
use self::plugins::PluginListFetchState;
use self::plugins::PluginsCacheState;
Expand Down Expand Up @@ -544,6 +545,8 @@ pub(crate) struct ChatWidget {
rate_limit_snapshots_by_limit_id: BTreeMap<String, RateLimitSnapshotDisplay>,
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
next_status_refresh_request_id: u64,
next_usage_request_id: u64,
active_usage_request_id: Option<u64>,
plan_type: Option<PlanType>,
codex_rate_limit_reached_type: Option<RateLimitReachedType>,
rate_limit_warnings: RateLimitWarningState,
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui/src/chatwidget/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ impl ChatWidget {
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
refreshing_status_outputs: Vec::new(),
next_status_refresh_request_id: 0,
next_usage_request_id: 0,
active_usage_request_id: None,
plan_type: initial_plan_type,
codex_rate_limit_reached_type: None,
rate_limit_warnings: RateLimitWarningState::default(),
Expand Down
15 changes: 15 additions & 0 deletions codex-rs/tui/src/chatwidget/slash_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ const GOAL_USAGE: &str = "Usage: /goal <objective>";
const GOAL_USAGE_HINT: &str = "Example: /goal improve benchmark coverage";
const RAW_USAGE: &str = "Usage: /raw [on|off]";

fn usage_range_from_arg(arg: &str) -> Option<codex_app_server_protocol::UsageRange> {
match arg.to_ascii_lowercase().as_str() {
"week" | "weekly" => Some(codex_app_server_protocol::UsageRange::Week),
_ => None,
}
}

impl ChatWidget {
/// Dispatch a bare slash command and record its staged local-history entry.
///
Expand Down Expand Up @@ -385,6 +392,9 @@ impl ChatWidget {
);
}
}
SlashCommand::Usage => {
self.add_usage_output();
}
SlashCommand::Ide => {
self.handle_ide_command();
}
Expand Down Expand Up @@ -617,6 +627,10 @@ impl ChatWidget {
}
_ => self.add_error_message(RAW_USAGE.to_string()),
},
SlashCommand::Usage => match usage_range_from_arg(trimmed) {
Some(range) => self.add_usage_output_for_range(range),
None => self.add_error_message("Usage: /usage [week|weekly]".to_string()),
},
SlashCommand::Rename if !trimmed.is_empty() => {
if !self.ensure_thread_rename_allowed() {
return;
Expand Down Expand Up @@ -932,6 +946,7 @@ impl ChatWidget {
match cmd {
SlashCommand::Ide
| SlashCommand::Status
| SlashCommand::Usage
| SlashCommand::DebugConfig
| SlashCommand::Ps
| SlashCommand::Stop
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: rendered
---
/usage

Usage by token share
Percent of consumed tokens in this selected range

Failed to load usage: sqlite state database could not be
opened because the configured path is unavailable
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: rendered
---
/usage

Daily usage by token share
Percent of consumed tokens in this selected range
(/usage week for weekly)

11% of consumed tokens came from app "testmcp"
Tool results stay in context until compaction;
compact or disable sources you do not need.

Skills
├─ /tmux 8% |██░░░░░░░░░░░░░░░░░░|
└─ /babysit 6% |██░░░░░░░░░░░░░░░░░░|

Subagents
├─ babysit 13% |███░░░░░░░░░░░░░░░░░|
└─ code-review 9% |██░░░░░░░░░░░░░░░░░░|

Agent tasks
├─ guardian 17% |████░░░░░░░░░░░░░░░░|
└─ review 5% |█░░░░░░░░░░░░░░░░░░░|

Apps
└─ testmcp 11% |███░░░░░░░░░░░░░░░░░|
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: rendered
---
/usage

Daily usage by token share
Percent of consumed tokens in this selected range
(/usage week for weekly)

No attributed skills, subagents, agent tasks,
apps, MCP servers, or plugins in this range.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: rendered
---
/usage week

Weekly usage by token share, Nov 7 to Nov 14
Percent of consumed tokens in this selected range

17% of consumed tokens came from agent task "guardian"

Skills
└─ /tmux 8% |██░░░░░░░░░░░░░░░░░░|

Subagents
└─ default 13% |███░░░░░░░░░░░░░░░░░|

Agent tasks
└─ guardian 17% |████░░░░░░░░░░░░░░░░|
6 changes: 6 additions & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ pub(super) use codex_app_server_protocol::TurnCompletedNotification;
pub(super) use codex_app_server_protocol::TurnError as AppServerTurnError;
pub(super) use codex_app_server_protocol::TurnStartedNotification;
pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
pub(super) use codex_app_server_protocol::UsageContributorKind;
pub(super) use codex_app_server_protocol::UsageEntry;
pub(super) use codex_app_server_protocol::UsageHeadline;
pub(super) use codex_app_server_protocol::UsageRange;
pub(super) use codex_app_server_protocol::UsageReadResponse;
pub(super) use codex_app_server_protocol::UsageReport;
pub(super) use codex_app_server_protocol::UserInput;
pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput;
pub(super) use codex_app_server_protocol::WarningNotification;
Expand Down
Loading
Loading