feat(app-server): expose usage report

This commit is contained in:
Felipe Coury
2026-05-29 13:45:26 -03:00
parent 3f0045c005
commit 8c5f7dd9da
21 changed files with 854 additions and 2 deletions
@@ -4092,6 +4092,24 @@
],
"type": "object"
},
"UsageRange": {
"enum": [
"day",
"week"
],
"type": "string"
},
"UsageReadParams": {
"properties": {
"range": {
"$ref": "#/definitions/UsageRange"
}
},
"required": [
"range"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -4891,6 +4909,30 @@
"title": "Plugin/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"usage/read"
],
"title": "Usage/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/UsageReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -829,6 +829,30 @@
"title": "Plugin/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"usage/read"
],
"title": "Usage/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/UsageReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -18601,6 +18625,177 @@
],
"type": "object"
},
"UsageContributorKind": {
"enum": [
"skill",
"subagent",
"agentTask",
"app",
"mcpServer",
"plugin"
],
"type": "string"
},
"UsageEntry": {
"properties": {
"attributedTokens": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/v2/UsageContributorKind"
},
"label": {
"type": "string"
},
"percentOfUsage": {
"format": "uint8",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"attributedTokens",
"id",
"kind",
"label",
"percentOfUsage"
],
"type": "object"
},
"UsageHeadline": {
"properties": {
"entry": {
"$ref": "#/definitions/v2/UsageEntry"
},
"note": {
"type": [
"string",
"null"
]
}
},
"required": [
"entry"
],
"type": "object"
},
"UsageRange": {
"enum": [
"day",
"week"
],
"type": "string"
},
"UsageReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"$ref": "#/definitions/v2/UsageRange"
}
},
"required": [
"range"
],
"title": "UsageReadParams",
"type": "object"
},
"UsageReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"report": {
"$ref": "#/definitions/v2/UsageReport"
}
},
"required": [
"report"
],
"title": "UsageReadResponse",
"type": "object"
},
"UsageReport": {
"properties": {
"agentTasks": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"generatedAt": {
"format": "int64",
"type": "integer"
},
"headline": {
"anyOf": [
{
"$ref": "#/definitions/v2/UsageHeadline"
},
{
"type": "null"
}
]
},
"mcpServers": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"plugins": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"range": {
"$ref": "#/definitions/v2/UsageRange"
},
"skills": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"subagents": {
"items": {
"$ref": "#/definitions/v2/UsageEntry"
},
"type": "array"
},
"totalTokens": {
"format": "int64",
"type": "integer"
},
"trackedFrom": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"agentTasks",
"apps",
"generatedAt",
"mcpServers",
"plugins",
"range",
"skills",
"subagents",
"totalTokens"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -1577,6 +1577,30 @@
"title": "Plugin/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"usage/read"
],
"title": "Usage/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/UsageReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -16425,6 +16449,177 @@
],
"type": "object"
},
"UsageContributorKind": {
"enum": [
"skill",
"subagent",
"agentTask",
"app",
"mcpServer",
"plugin"
],
"type": "string"
},
"UsageEntry": {
"properties": {
"attributedTokens": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/UsageContributorKind"
},
"label": {
"type": "string"
},
"percentOfUsage": {
"format": "uint8",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"attributedTokens",
"id",
"kind",
"label",
"percentOfUsage"
],
"type": "object"
},
"UsageHeadline": {
"properties": {
"entry": {
"$ref": "#/definitions/UsageEntry"
},
"note": {
"type": [
"string",
"null"
]
}
},
"required": [
"entry"
],
"type": "object"
},
"UsageRange": {
"enum": [
"day",
"week"
],
"type": "string"
},
"UsageReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"$ref": "#/definitions/UsageRange"
}
},
"required": [
"range"
],
"title": "UsageReadParams",
"type": "object"
},
"UsageReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"report": {
"$ref": "#/definitions/UsageReport"
}
},
"required": [
"report"
],
"title": "UsageReadResponse",
"type": "object"
},
"UsageReport": {
"properties": {
"agentTasks": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"generatedAt": {
"format": "int64",
"type": "integer"
},
"headline": {
"anyOf": [
{
"$ref": "#/definitions/UsageHeadline"
},
{
"type": "null"
}
]
},
"mcpServers": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"plugins": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"range": {
"$ref": "#/definitions/UsageRange"
},
"skills": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"subagents": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"totalTokens": {
"format": "int64",
"type": "integer"
},
"trackedFrom": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"agentTasks",
"apps",
"generatedAt",
"mcpServers",
"plugins",
"range",
"skills",
"subagents",
"totalTokens"
],
"type": "object"
},
"UserInput": {
"oneOf": [
{
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UsageRange": {
"enum": [
"day",
"week"
],
"type": "string"
}
},
"properties": {
"range": {
"$ref": "#/definitions/UsageRange"
}
},
"required": [
"range"
],
"title": "UsageReadParams",
"type": "object"
}
@@ -0,0 +1,160 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UsageContributorKind": {
"enum": [
"skill",
"subagent",
"agentTask",
"app",
"mcpServer",
"plugin"
],
"type": "string"
},
"UsageEntry": {
"properties": {
"attributedTokens": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/UsageContributorKind"
},
"label": {
"type": "string"
},
"percentOfUsage": {
"format": "uint8",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"attributedTokens",
"id",
"kind",
"label",
"percentOfUsage"
],
"type": "object"
},
"UsageHeadline": {
"properties": {
"entry": {
"$ref": "#/definitions/UsageEntry"
},
"note": {
"type": [
"string",
"null"
]
}
},
"required": [
"entry"
],
"type": "object"
},
"UsageRange": {
"enum": [
"day",
"week"
],
"type": "string"
},
"UsageReport": {
"properties": {
"agentTasks": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"generatedAt": {
"format": "int64",
"type": "integer"
},
"headline": {
"anyOf": [
{
"$ref": "#/definitions/UsageHeadline"
},
{
"type": "null"
}
]
},
"mcpServers": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"plugins": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"range": {
"$ref": "#/definitions/UsageRange"
},
"skills": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"subagents": {
"items": {
"$ref": "#/definitions/UsageEntry"
},
"type": "array"
},
"totalTokens": {
"format": "int64",
"type": "integer"
},
"trackedFrom": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"agentTasks",
"apps",
"generatedAt",
"mcpServers",
"plugins",
"range",
"skills",
"subagents",
"totalTokens"
],
"type": "object"
}
},
"properties": {
"report": {
"$ref": "#/definitions/UsageReport"
}
},
"required": [
"report"
],
"title": "UsageReadResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UsageContributorKind = "skill" | "subagent" | "agentTask" | "app" | "mcpServer" | "plugin";
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageContributorKind } from "./UsageContributorKind";
export type UsageEntry = { kind: UsageContributorKind, id: string, label: string, attributedTokens: number, percentOfUsage: number, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageEntry } from "./UsageEntry";
export type UsageHeadline = { entry: UsageEntry, note: string | null, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UsageRange = "day" | "week";
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageRange } from "./UsageRange";
export type UsageReadParams = { range: UsageRange, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageReport } from "./UsageReport";
export type UsageReadResponse = { report: UsageReport, };
@@ -0,0 +1,8 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageEntry } from "./UsageEntry";
import type { UsageHeadline } from "./UsageHeadline";
import type { UsageRange } from "./UsageRange";
export type UsageReport = { range: UsageRange, generatedAt: number, trackedFrom: number | null, totalTokens: number, headline: UsageHeadline | null, skills: Array<UsageEntry>, subagents: Array<UsageEntry>, agentTasks: Array<UsageEntry>, apps: Array<UsageEntry>, mcpServers: Array<UsageEntry>, plugins: Array<UsageEntry>, };
@@ -452,6 +452,13 @@ export type { TurnStatus } from "./TurnStatus";
export type { TurnSteerParams } from "./TurnSteerParams";
export type { TurnSteerResponse } from "./TurnSteerResponse";
export type { TurnsPage } from "./TurnsPage";
export type { UsageContributorKind } from "./UsageContributorKind";
export type { UsageEntry } from "./UsageEntry";
export type { UsageHeadline } from "./UsageHeadline";
export type { UsageRange } from "./UsageRange";
export type { UsageReadParams } from "./UsageReadParams";
export type { UsageReadResponse } from "./UsageReadResponse";
export type { UsageReport } from "./UsageReport";
export type { UserInput } from "./UserInput";
export type { WarningNotification } from "./WarningNotification";
export type { WebSearchAction } from "./WebSearchAction";
@@ -635,6 +635,11 @@ client_request_definitions! {
serialization: None,
response: v2::PluginListResponse,
},
UsageRead => "usage/read" {
params: v2::UsageReadParams,
serialization: None,
response: v2::UsageReadResponse,
},
PluginInstalled => "plugin/installed" {
params: v2::PluginInstalledParams,
serialization: None,
@@ -24,6 +24,7 @@ mod review;
mod thread;
mod thread_data;
mod turn;
mod usage;
mod windows_sandbox;
pub use account::*;
@@ -51,6 +52,7 @@ pub use shared::*;
pub use thread::*;
pub use thread_data::*;
pub use turn::*;
pub use usage::*;
pub use windows_sandbox::*;
#[cfg(test)]
@@ -0,0 +1,92 @@
use codex_protocol::protocol::UsageContributorKind as CoreUsageContributorKind;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum UsageRange {
Day,
Week,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct UsageReadParams {
pub range: UsageRange,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct UsageEntry {
pub kind: UsageContributorKind,
pub id: String,
pub label: String,
#[ts(type = "number")]
pub attributed_tokens: i64,
pub percent_of_usage: u8,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct UsageHeadline {
pub entry: UsageEntry,
pub note: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct UsageReport {
pub range: UsageRange,
#[ts(type = "number")]
pub generated_at: i64,
#[ts(type = "number | null")]
pub tracked_from: Option<i64>,
#[ts(type = "number")]
pub total_tokens: i64,
pub headline: Option<UsageHeadline>,
pub skills: Vec<UsageEntry>,
pub subagents: Vec<UsageEntry>,
pub agent_tasks: Vec<UsageEntry>,
pub apps: Vec<UsageEntry>,
pub mcp_servers: Vec<UsageEntry>,
pub plugins: Vec<UsageEntry>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct UsageReadResponse {
pub report: UsageReport,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub enum UsageContributorKind {
Skill,
Subagent,
AgentTask,
App,
McpServer,
Plugin,
}
impl From<CoreUsageContributorKind> for UsageContributorKind {
fn from(value: CoreUsageContributorKind) -> Self {
match value {
CoreUsageContributorKind::Skill => Self::Skill,
CoreUsageContributorKind::Subagent => Self::Subagent,
CoreUsageContributorKind::AgentTask => Self::AgentTask,
CoreUsageContributorKind::App => Self::App,
CoreUsageContributorKind::McpServer => Self::McpServer,
CoreUsageContributorKind::Plugin => Self::Plugin,
}
}
}
+1
View File
@@ -206,6 +206,7 @@ Example with notification opt-out:
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
- `skills/changed` — notification emitted when watched local skill files change.
- `app/list` — list available apps.
- `usage/read` — read forward-only local token usage for a rolling `day` or `week` range, grouped by skills, subagents, agent tasks, apps, MCP servers, and plugins when Codex has tracked those contributors in sqlite.
- `remoteControl/enable` — experimental; enable remote control for the current app-server process and return the current remote-control status snapshot. The caller is responsible for persisting the desired setting outside app-server.
- `remoteControl/disable` — experimental; disable remote control for the current app-server process and return the current remote-control status snapshot. This does not revoke already enrolled controller devices.
- `remoteControl/status/read` — experimental; read the current remote-control status snapshot. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled.
+10 -1
View File
@@ -36,6 +36,7 @@ use crate::request_processors::SearchRequestProcessor;
use crate::request_processors::ThreadGoalRequestProcessor;
use crate::request_processors::ThreadRequestProcessor;
use crate::request_processors::TurnRequestProcessor;
use crate::request_processors::UsageRequestProcessor;
use crate::request_processors::WindowsSandboxRequestProcessor;
use crate::request_serialization::QueuedInitializedRequest;
use crate::request_serialization::RequestSerializationQueueKey;
@@ -182,6 +183,7 @@ pub(crate) struct MessageProcessor {
thread_goal_processor: ThreadGoalRequestProcessor,
thread_processor: ThreadRequestProcessor,
turn_processor: TurnRequestProcessor,
usage_processor: UsageRequestProcessor,
windows_sandbox_processor: WindowsSandboxRequestProcessor,
request_serialization_queues: RequestSerializationQueues,
}
@@ -428,9 +430,10 @@ impl MessageProcessor {
thread_watch_manager.clone(),
Arc::clone(&thread_list_state_permit),
thread_goal_processor.clone(),
state_db,
state_db.clone(),
Arc::clone(&skills_watcher),
);
let usage_processor = UsageRequestProcessor::new(state_db);
let turn_processor = TurnRequestProcessor::new(
auth_manager.clone(),
Arc::clone(&thread_manager),
@@ -507,6 +510,7 @@ impl MessageProcessor {
thread_goal_processor,
thread_processor,
turn_processor,
usage_processor,
windows_sandbox_processor,
request_serialization_queues: RequestSerializationQueues::default(),
}
@@ -1123,6 +1127,11 @@ impl MessageProcessor {
ClientRequest::PluginList { params, .. } => {
self.plugin_processor.plugin_list(params).await
}
ClientRequest::UsageRead { params, .. } => self
.usage_processor
.usage_read(params)
.await
.map(|response| Some(response.into())),
ClientRequest::PluginInstalled { params, .. } => {
self.plugin_processor.plugin_installed(params).await
}
@@ -474,6 +474,7 @@ mod search;
mod thread_processor;
mod token_usage_replay;
mod turn_processor;
mod usage_processor;
mod windows_sandbox_processor;
pub(crate) use account_processor::AccountRequestProcessor;
@@ -496,6 +497,7 @@ pub(crate) use search::SearchRequestProcessor;
pub(crate) use thread_goal_processor::ThreadGoalRequestProcessor;
pub(crate) use thread_processor::ThreadRequestProcessor;
pub(crate) use turn_processor::TurnRequestProcessor;
pub(crate) use usage_processor::UsageRequestProcessor;
pub(crate) use windows_sandbox_processor::WindowsSandboxRequestProcessor;
use crate::error_code::internal_error;
@@ -0,0 +1,77 @@
use super::*;
use crate::error_code::internal_error;
use chrono::Utc;
use codex_app_server_protocol::UsageEntry;
use codex_app_server_protocol::UsageHeadline;
use codex_app_server_protocol::UsageRange;
use codex_app_server_protocol::UsageReadParams;
use codex_app_server_protocol::UsageReadResponse;
use codex_app_server_protocol::UsageReport;
use codex_rollout::StateDbHandle;
#[derive(Clone)]
pub(crate) struct UsageRequestProcessor {
state_db: Option<StateDbHandle>,
}
impl UsageRequestProcessor {
pub(crate) fn new(state_db: Option<StateDbHandle>) -> Self {
Self { state_db }
}
pub(crate) async fn usage_read(
&self,
params: UsageReadParams,
) -> Result<UsageReadResponse, JSONRPCErrorError> {
let state_db = self
.state_db
.as_ref()
.ok_or_else(|| internal_error("sqlite state db unavailable for usage"))?;
let report = state_db
.read_usage_report(state_usage_range(params.range), Utc::now().timestamp())
.await
.map_err(|err| internal_error(format!("failed to read usage report: {err}")))?;
Ok(UsageReadResponse {
report: UsageReport {
range: api_usage_range(report.range),
generated_at: report.generated_at,
tracked_from: report.tracked_from,
total_tokens: report.total_tokens,
headline: report.headline.map(|headline| UsageHeadline {
entry: usage_entry(headline.entry),
note: headline.note,
}),
skills: report.skills.into_iter().map(usage_entry).collect(),
subagents: report.subagents.into_iter().map(usage_entry).collect(),
agent_tasks: report.agent_tasks.into_iter().map(usage_entry).collect(),
apps: report.apps.into_iter().map(usage_entry).collect(),
mcp_servers: report.mcp_servers.into_iter().map(usage_entry).collect(),
plugins: report.plugins.into_iter().map(usage_entry).collect(),
},
})
}
}
fn usage_entry(entry: codex_state::UsageEntry) -> UsageEntry {
UsageEntry {
kind: entry.kind.into(),
id: entry.id,
label: entry.label,
attributed_tokens: entry.attributed_tokens,
percent_of_usage: entry.percent_of_usage,
}
}
fn state_usage_range(value: UsageRange) -> codex_state::UsageRange {
match value {
UsageRange::Day => codex_state::UsageRange::Day,
UsageRange::Week => codex_state::UsageRange::Week,
}
}
fn api_usage_range(value: codex_state::UsageRange) -> UsageRange {
match value {
codex_state::UsageRange::Day => UsageRange::Day,
codex_state::UsageRange::Week => UsageRange::Week,
}
}