diff --git a/Cargo.toml b/Cargo.toml index 592903db..0bc08b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ unstable = [ "unstable_session_list", "unstable_session_model", "unstable_session_resume", + "unstable_session_usage", ] unstable_cancel_request = [] unstable_session_config_options = [] @@ -30,6 +31,7 @@ unstable_session_info_update = [] unstable_session_list = [] unstable_session_model = [] unstable_session_resume = [] +unstable_session_usage = [] [[bin]] name = "generate" diff --git a/src/agent.rs b/src/agent.rs index 6f5e5627..0db402a0 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -2118,6 +2118,10 @@ impl PromptRequest { pub struct PromptResponse { /// Indicates why the agent stopped processing the turn. pub stop_reason: StopReason, + /// Token usage for this turn (optional). + #[cfg(feature = "unstable_session_usage")] + #[serde(skip_serializing_if = "Option::is_none")] + pub usage: Option, /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -2132,10 +2136,20 @@ impl PromptResponse { pub fn new(stop_reason: StopReason) -> Self { Self { stop_reason, + #[cfg(feature = "unstable_session_usage")] + usage: None, meta: None, } } + /// Token usage for this turn. + #[cfg(feature = "unstable_session_usage")] + #[must_use] + pub fn usage(mut self, usage: impl IntoOption) -> Self { + self.usage = usage.into_option(); + self + } + /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -2175,6 +2189,65 @@ pub enum StopReason { Cancelled, } +/// Token usage information for a prompt turn. +#[cfg(feature = "unstable_session_usage")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Usage { + /// Sum of all token types across session. + pub total_tokens: u64, + /// Total input tokens across all turns. + pub input_tokens: u64, + /// Total output tokens across all turns. + pub output_tokens: u64, + /// Total thought/reasoning tokens (for o1/o3 models). + #[serde(skip_serializing_if = "Option::is_none")] + pub thought_tokens: Option, + /// Total cache read tokens. + #[serde(skip_serializing_if = "Option::is_none")] + pub cached_read_tokens: Option, + /// Total cache write tokens. + #[serde(skip_serializing_if = "Option::is_none")] + pub cached_write_tokens: Option, +} + +#[cfg(feature = "unstable_session_usage")] +impl Usage { + #[must_use] + pub fn new(total_tokens: u64, input_tokens: u64, output_tokens: u64) -> Self { + Self { + total_tokens, + input_tokens, + output_tokens, + thought_tokens: None, + cached_read_tokens: None, + cached_write_tokens: None, + } + } + + /// Total thought/reasoning tokens (for o1/o3 models). + #[must_use] + pub fn thought_tokens(mut self, thought_tokens: impl IntoOption) -> Self { + self.thought_tokens = thought_tokens.into_option(); + self + } + + /// Total cache read tokens. + #[must_use] + pub fn cached_read_tokens(mut self, cached_read_tokens: impl IntoOption) -> Self { + self.cached_read_tokens = cached_read_tokens.into_option(); + self + } + + /// Total cache write tokens. + #[must_use] + pub fn cached_write_tokens(mut self, cached_write_tokens: impl IntoOption) -> Self { + self.cached_write_tokens = cached_write_tokens.into_option(); + self + } +} + // Model /// **UNSTABLE** diff --git a/src/client.rs b/src/client.rs index 94cf5644..74369049 100644 --- a/src/client.rs +++ b/src/client.rs @@ -104,6 +104,9 @@ pub enum SessionUpdate { #[cfg(feature = "unstable_session_info_update")] /// Session metadata has been updated (title, timestamps, custom metadata) SessionInfoUpdate(SessionInfoUpdate), + /// Context window and cost update for the session. + #[cfg(feature = "unstable_session_usage")] + UsageUpdate(UsageUpdate), } /// The current mode of the session has changed @@ -245,6 +248,82 @@ impl SessionInfoUpdate { } } +/// Context window and cost update for a session. +#[cfg(feature = "unstable_session_usage")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct UsageUpdate { + /// Tokens currently in context. + pub used: u64, + /// Total context window size in tokens. + pub size: u64, + /// Cumulative session cost (optional). + #[serde(skip_serializing_if = "Option::is_none")] + pub cost: Option, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_session_usage")] +impl UsageUpdate { + #[must_use] + pub fn new(used: u64, size: u64) -> Self { + Self { + used, + size, + cost: None, + meta: None, + } + } + + /// Cumulative session cost (optional). + #[must_use] + pub fn cost(mut self, cost: impl IntoOption) -> Self { + self.cost = cost.into_option(); + self + } + + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Cost information for a session. +#[cfg(feature = "unstable_session_usage")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Cost { + /// Total cumulative cost for session. + pub amount: f64, + /// ISO 4217 currency code (e.g., "USD", "EUR"). + pub currency: String, +} + +#[cfg(feature = "unstable_session_usage")] +impl Cost { + #[must_use] + pub fn new(amount: f64, currency: impl Into) -> Self { + Self { + amount, + currency: currency.into(), + } + } +} + /// A streamed item of content #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")]